สวัสดีครับทุกท่านที่กำลังอ่านอยู่ 😀
สำหรับ Project นี้ผมจะสมมุติบทบาทตัวเองว่ากำลังทำงาน Data Analyst ในบริษัทบริหารสินทรัพย์แห่งหนึ่ง โดยธุรกิจหลักของบริษัทนี้คือการซื้อหนี้เสียแบบมีหลักประกันจากสถาบันการเงินต่างๆ โดยหลักประกันในทีนี้คืออสังหาริมทรัพย์
โดยทางเลือกที่ทางบริษัทจะมอบให้แก่ลูกหนี้ที่ผิดชำระมีสองทางเลือกได้แก่
โดย Project ที่ผมกำลังโฟกัสอยู่นี้เป็นเรื่องของการทำความเข้าใจตลาดอสังหาริมทรัพย์ รวมไปถึงหา Insight ต่างๆ จากข้อมูลดิบที่มีอยู่ โดยเฉพาะพฤติกรรมของผู้ซื้อบ้านเป็นหลัก
เพราะการเข้าใจพฤติกรรมผู้ซื้อบ้าน และการทำความเข้าใจแนวโน้มต่างๆ ของโครงการอสังหาริมทรัพย์ในท้องที่ รวมไปถึง Factor ต่างๆ ที่มีผลกับราคาบ้าน เป็นอะไรที่สำคัญอย่างยิ่งสำหรับธุรกิจ
เนื่องจากว่าถ้าเราได้หลักประกันในราคาที่แพงเกินไป จะส่งผลต่อกำไรของบริษัทได้
โดย Dataset คราวนี้ที่ผมนำมาใช้
ผมนำมาจาก Kaggle ตาม Link นี้เลยครับ
https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques/data
เพื่อไม่ให้เป็นการเสียเวลา มาเริ่มกันเลย !
ขั้นตอนแรกในการทำงานกับข้อมูลคือการจัดการกับข้อมูล ไล่ตั้งแต่
โดยเครื่องมือยอดฮิตสำหรับงานนี้จะเป็นใครไปไม่ได้นอกจาก Pandas
import pandas as pd
df=pd.read_csv('/content/drive/MyDrive/EDA House Price with Python/train.csv')
df.head()
| Id | MSSubClass | MSZoning | LotFrontage | LotArea | Street | Alley | LotShape | LandContour | Utilities | ... | PoolArea | PoolQC | Fence | MiscFeature | MiscVal | MoSold | YrSold | SaleType | SaleCondition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 60 | RL | 65.0 | 8450 | Pave | NaN | Reg | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 2 | 2008 | WD | Normal | 208500 |
| 1 | 2 | 20 | RL | 80.0 | 9600 | Pave | NaN | Reg | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2007 | WD | Normal | 181500 |
| 2 | 3 | 60 | RL | 68.0 | 11250 | Pave | NaN | IR1 | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 9 | 2008 | WD | Normal | 223500 |
| 3 | 4 | 70 | RL | 60.0 | 9550 | Pave | NaN | IR1 | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 2 | 2006 | WD | Abnorml | 140000 |
| 4 | 5 | 60 | RL | 84.0 | 14260 | Pave | NaN | IR1 | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 12 | 2008 | WD | Normal | 250000 |
5 rows × 81 columns
ลองพิมพ์ df.info() สักหน่อยเพื่อดูคร่าวๆว่าตัวแปรต่างๆ ถูกเก็บในรูปแบบใด
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1460 entries, 0 to 1459 Data columns (total 81 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Id 1460 non-null int64 1 MSSubClass 1460 non-null int64 2 MSZoning 1460 non-null object 3 LotFrontage 1201 non-null float64 4 LotArea 1460 non-null int64 5 Street 1460 non-null object 6 Alley 91 non-null object 7 LotShape 1460 non-null object 8 LandContour 1460 non-null object 9 Utilities 1460 non-null object 10 LotConfig 1460 non-null object 11 LandSlope 1460 non-null object 12 Neighborhood 1460 non-null object 13 Condition1 1460 non-null object 14 Condition2 1460 non-null object 15 BldgType 1460 non-null object 16 HouseStyle 1460 non-null object 17 OverallQual 1460 non-null int64 18 OverallCond 1460 non-null int64 19 YearBuilt 1460 non-null int64 20 YearRemodAdd 1460 non-null int64 21 RoofStyle 1460 non-null object 22 RoofMatl 1460 non-null object 23 Exterior1st 1460 non-null object 24 Exterior2nd 1460 non-null object 25 MasVnrType 1452 non-null object 26 MasVnrArea 1452 non-null float64 27 ExterQual 1460 non-null object 28 ExterCond 1460 non-null object 29 Foundation 1460 non-null object 30 BsmtQual 1423 non-null object 31 BsmtCond 1423 non-null object 32 BsmtExposure 1422 non-null object 33 BsmtFinType1 1423 non-null object 34 BsmtFinSF1 1460 non-null int64 35 BsmtFinType2 1422 non-null object 36 BsmtFinSF2 1460 non-null int64 37 BsmtUnfSF 1460 non-null int64 38 TotalBsmtSF 1460 non-null int64 39 Heating 1460 non-null object 40 HeatingQC 1460 non-null object 41 CentralAir 1460 non-null object 42 Electrical 1459 non-null object 43 1stFlrSF 1460 non-null int64 44 2ndFlrSF 1460 non-null int64 45 LowQualFinSF 1460 non-null int64 46 GrLivArea 1460 non-null int64 47 BsmtFullBath 1460 non-null int64 48 BsmtHalfBath 1460 non-null int64 49 FullBath 1460 non-null int64 50 HalfBath 1460 non-null int64 51 BedroomAbvGr 1460 non-null int64 52 KitchenAbvGr 1460 non-null int64 53 KitchenQual 1460 non-null object 54 TotRmsAbvGrd 1460 non-null int64 55 Functional 1460 non-null object 56 Fireplaces 1460 non-null int64 57 FireplaceQu 770 non-null object 58 GarageType 1379 non-null object 59 GarageYrBlt 1379 non-null float64 60 GarageFinish 1379 non-null object 61 GarageCars 1460 non-null int64 62 GarageArea 1460 non-null int64 63 GarageQual 1379 non-null object 64 GarageCond 1379 non-null object 65 PavedDrive 1460 non-null object 66 WoodDeckSF 1460 non-null int64 67 OpenPorchSF 1460 non-null int64 68 EnclosedPorch 1460 non-null int64 69 3SsnPorch 1460 non-null int64 70 ScreenPorch 1460 non-null int64 71 PoolArea 1460 non-null int64 72 PoolQC 7 non-null object 73 Fence 281 non-null object 74 MiscFeature 54 non-null object 75 MiscVal 1460 non-null int64 76 MoSold 1460 non-null int64 77 YrSold 1460 non-null int64 78 SaleType 1460 non-null object 79 SaleCondition 1460 non-null object 80 SalePrice 1460 non-null int64 dtypes: float64(3), int64(35), object(43) memory usage: 924.0+ KB
โห มีตั้ง 80 Column อ่านยากเกิน ขอสลับตารางนิดนึงด้วยการ Transpose จากนั้นใช้คำสั่ง describe เพื่อดูค่าสถิติพื้นฐาน
df.describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| Id | 1460.0 | 730.500000 | 421.610009 | 1.0 | 365.75 | 730.5 | 1095.25 | 1460.0 |
| MSSubClass | 1460.0 | 56.897260 | 42.300571 | 20.0 | 20.00 | 50.0 | 70.00 | 190.0 |
| LotFrontage | 1201.0 | 70.049958 | 24.284752 | 21.0 | 59.00 | 69.0 | 80.00 | 313.0 |
| LotArea | 1460.0 | 10516.828082 | 9981.264932 | 1300.0 | 7553.50 | 9478.5 | 11601.50 | 215245.0 |
| OverallQual | 1460.0 | 6.099315 | 1.382997 | 1.0 | 5.00 | 6.0 | 7.00 | 10.0 |
| OverallCond | 1460.0 | 5.575342 | 1.112799 | 1.0 | 5.00 | 5.0 | 6.00 | 9.0 |
| YearBuilt | 1460.0 | 1971.267808 | 30.202904 | 1872.0 | 1954.00 | 1973.0 | 2000.00 | 2010.0 |
| YearRemodAdd | 1460.0 | 1984.865753 | 20.645407 | 1950.0 | 1967.00 | 1994.0 | 2004.00 | 2010.0 |
| MasVnrArea | 1452.0 | 103.685262 | 181.066207 | 0.0 | 0.00 | 0.0 | 166.00 | 1600.0 |
| BsmtFinSF1 | 1460.0 | 443.639726 | 456.098091 | 0.0 | 0.00 | 383.5 | 712.25 | 5644.0 |
| BsmtFinSF2 | 1460.0 | 46.549315 | 161.319273 | 0.0 | 0.00 | 0.0 | 0.00 | 1474.0 |
| BsmtUnfSF | 1460.0 | 567.240411 | 441.866955 | 0.0 | 223.00 | 477.5 | 808.00 | 2336.0 |
| TotalBsmtSF | 1460.0 | 1057.429452 | 438.705324 | 0.0 | 795.75 | 991.5 | 1298.25 | 6110.0 |
| 1stFlrSF | 1460.0 | 1162.626712 | 386.587738 | 334.0 | 882.00 | 1087.0 | 1391.25 | 4692.0 |
| 2ndFlrSF | 1460.0 | 346.992466 | 436.528436 | 0.0 | 0.00 | 0.0 | 728.00 | 2065.0 |
| LowQualFinSF | 1460.0 | 5.844521 | 48.623081 | 0.0 | 0.00 | 0.0 | 0.00 | 572.0 |
| GrLivArea | 1460.0 | 1515.463699 | 525.480383 | 334.0 | 1129.50 | 1464.0 | 1776.75 | 5642.0 |
| BsmtFullBath | 1460.0 | 0.425342 | 0.518911 | 0.0 | 0.00 | 0.0 | 1.00 | 3.0 |
| BsmtHalfBath | 1460.0 | 0.057534 | 0.238753 | 0.0 | 0.00 | 0.0 | 0.00 | 2.0 |
| FullBath | 1460.0 | 1.565068 | 0.550916 | 0.0 | 1.00 | 2.0 | 2.00 | 3.0 |
| HalfBath | 1460.0 | 0.382877 | 0.502885 | 0.0 | 0.00 | 0.0 | 1.00 | 2.0 |
| BedroomAbvGr | 1460.0 | 2.866438 | 0.815778 | 0.0 | 2.00 | 3.0 | 3.00 | 8.0 |
| KitchenAbvGr | 1460.0 | 1.046575 | 0.220338 | 0.0 | 1.00 | 1.0 | 1.00 | 3.0 |
| TotRmsAbvGrd | 1460.0 | 6.517808 | 1.625393 | 2.0 | 5.00 | 6.0 | 7.00 | 14.0 |
| Fireplaces | 1460.0 | 0.613014 | 0.644666 | 0.0 | 0.00 | 1.0 | 1.00 | 3.0 |
| GarageYrBlt | 1379.0 | 1978.506164 | 24.689725 | 1900.0 | 1961.00 | 1980.0 | 2002.00 | 2010.0 |
| GarageCars | 1460.0 | 1.767123 | 0.747315 | 0.0 | 1.00 | 2.0 | 2.00 | 4.0 |
| GarageArea | 1460.0 | 472.980137 | 213.804841 | 0.0 | 334.50 | 480.0 | 576.00 | 1418.0 |
| WoodDeckSF | 1460.0 | 94.244521 | 125.338794 | 0.0 | 0.00 | 0.0 | 168.00 | 857.0 |
| OpenPorchSF | 1460.0 | 46.660274 | 66.256028 | 0.0 | 0.00 | 25.0 | 68.00 | 547.0 |
| EnclosedPorch | 1460.0 | 21.954110 | 61.119149 | 0.0 | 0.00 | 0.0 | 0.00 | 552.0 |
| 3SsnPorch | 1460.0 | 3.409589 | 29.317331 | 0.0 | 0.00 | 0.0 | 0.00 | 508.0 |
| ScreenPorch | 1460.0 | 15.060959 | 55.757415 | 0.0 | 0.00 | 0.0 | 0.00 | 480.0 |
| PoolArea | 1460.0 | 2.758904 | 40.177307 | 0.0 | 0.00 | 0.0 | 0.00 | 738.0 |
| MiscVal | 1460.0 | 43.489041 | 496.123024 | 0.0 | 0.00 | 0.0 | 0.00 | 15500.0 |
| MoSold | 1460.0 | 6.321918 | 2.703626 | 1.0 | 5.00 | 6.0 | 8.00 | 12.0 |
| YrSold | 1460.0 | 2007.815753 | 1.328095 | 2006.0 | 2007.00 | 2008.0 | 2009.00 | 2010.0 |
| SalePrice | 1460.0 | 180921.195890 | 79442.502883 | 34900.0 | 129975.00 | 163000.0 | 214000.00 | 755000.0 |
ลองตรวจสอบค่าในที่มีความเฉพาะในแต่ละ Columns ดูดีกว่าว่าในนั้นมตัวแปรอะไรเก็บไว้อยู่บ้าน โดยเราจะเริ่มจาก
โดยผลลัพธ์ที่ได้จะอยู่ด้านล่าง
# Get only columns of object type
object_cols = df.select_dtypes(include=['object']).columns
# Iterate over object columns and print unique values
for col in object_cols:
print(f"Column '{col}' unique values: ", df[col].unique())
Column 'MSZoning' unique values: ['RL' 'RM' 'C (all)' 'FV' 'RH'] Column 'Street' unique values: ['Pave' 'Grvl'] Column 'Alley' unique values: [nan 'Grvl' 'Pave'] Column 'LotShape' unique values: ['Reg' 'IR1' 'IR2' 'IR3'] Column 'LandContour' unique values: ['Lvl' 'Bnk' 'Low' 'HLS'] Column 'Utilities' unique values: ['AllPub' 'NoSeWa'] Column 'LotConfig' unique values: ['Inside' 'FR2' 'Corner' 'CulDSac' 'FR3'] Column 'LandSlope' unique values: ['Gtl' 'Mod' 'Sev'] Column 'Neighborhood' unique values: ['CollgCr' 'Veenker' 'Crawfor' 'NoRidge' 'Mitchel' 'Somerst' 'NWAmes' 'OldTown' 'BrkSide' 'Sawyer' 'NridgHt' 'NAmes' 'SawyerW' 'IDOTRR' 'MeadowV' 'Edwards' 'Timber' 'Gilbert' 'StoneBr' 'ClearCr' 'NPkVill' 'Blmngtn' 'BrDale' 'SWISU' 'Blueste'] Column 'Condition1' unique values: ['Norm' 'Feedr' 'PosN' 'Artery' 'RRAe' 'RRNn' 'RRAn' 'PosA' 'RRNe'] Column 'Condition2' unique values: ['Norm' 'Artery' 'RRNn' 'Feedr' 'PosN' 'PosA' 'RRAn' 'RRAe'] Column 'BldgType' unique values: ['1Fam' '2fmCon' 'Duplex' 'TwnhsE' 'Twnhs'] Column 'HouseStyle' unique values: ['2Story' '1Story' '1.5Fin' '1.5Unf' 'SFoyer' 'SLvl' '2.5Unf' '2.5Fin'] Column 'RoofStyle' unique values: ['Gable' 'Hip' 'Gambrel' 'Mansard' 'Flat' 'Shed'] Column 'RoofMatl' unique values: ['CompShg' 'WdShngl' 'Metal' 'WdShake' 'Membran' 'Tar&Grv' 'Roll' 'ClyTile'] Column 'Exterior1st' unique values: ['VinylSd' 'MetalSd' 'Wd Sdng' 'HdBoard' 'BrkFace' 'WdShing' 'CemntBd' 'Plywood' 'AsbShng' 'Stucco' 'BrkComm' 'AsphShn' 'Stone' 'ImStucc' 'CBlock'] Column 'Exterior2nd' unique values: ['VinylSd' 'MetalSd' 'Wd Shng' 'HdBoard' 'Plywood' 'Wd Sdng' 'CmentBd' 'BrkFace' 'Stucco' 'AsbShng' 'Brk Cmn' 'ImStucc' 'AsphShn' 'Stone' 'Other' 'CBlock'] Column 'MasVnrType' unique values: ['BrkFace' 'None' 'Stone' 'BrkCmn' nan] Column 'ExterQual' unique values: ['Gd' 'TA' 'Ex' 'Fa'] Column 'ExterCond' unique values: ['TA' 'Gd' 'Fa' 'Po' 'Ex'] Column 'Foundation' unique values: ['PConc' 'CBlock' 'BrkTil' 'Wood' 'Slab' 'Stone'] Column 'BsmtQual' unique values: ['Gd' 'TA' 'Ex' nan 'Fa'] Column 'BsmtCond' unique values: ['TA' 'Gd' nan 'Fa' 'Po'] Column 'BsmtExposure' unique values: ['No' 'Gd' 'Mn' 'Av' nan] Column 'BsmtFinType1' unique values: ['GLQ' 'ALQ' 'Unf' 'Rec' 'BLQ' nan 'LwQ'] Column 'BsmtFinType2' unique values: ['Unf' 'BLQ' nan 'ALQ' 'Rec' 'LwQ' 'GLQ'] Column 'Heating' unique values: ['GasA' 'GasW' 'Grav' 'Wall' 'OthW' 'Floor'] Column 'HeatingQC' unique values: ['Ex' 'Gd' 'TA' 'Fa' 'Po'] Column 'CentralAir' unique values: ['Y' 'N'] Column 'Electrical' unique values: ['SBrkr' 'FuseF' 'FuseA' 'FuseP' 'Mix' nan] Column 'KitchenQual' unique values: ['Gd' 'TA' 'Ex' 'Fa'] Column 'Functional' unique values: ['Typ' 'Min1' 'Maj1' 'Min2' 'Mod' 'Maj2' 'Sev'] Column 'FireplaceQu' unique values: [nan 'TA' 'Gd' 'Fa' 'Ex' 'Po'] Column 'GarageType' unique values: ['Attchd' 'Detchd' 'BuiltIn' 'CarPort' nan 'Basment' '2Types'] Column 'GarageFinish' unique values: ['RFn' 'Unf' 'Fin' nan] Column 'GarageQual' unique values: ['TA' 'Fa' 'Gd' nan 'Ex' 'Po'] Column 'GarageCond' unique values: ['TA' 'Fa' nan 'Gd' 'Po' 'Ex'] Column 'PavedDrive' unique values: ['Y' 'N' 'P'] Column 'PoolQC' unique values: [nan 'Ex' 'Fa' 'Gd'] Column 'Fence' unique values: [nan 'MnPrv' 'GdWo' 'GdPrv' 'MnWw'] Column 'MiscFeature' unique values: [nan 'Shed' 'Gar2' 'Othr' 'TenC'] Column 'SaleType' unique values: ['WD' 'New' 'COD' 'ConLD' 'ConLI' 'CWD' 'ConLw' 'Con' 'Oth'] Column 'SaleCondition' unique values: ['Normal' 'Abnorml' 'Partial' 'AdjLand' 'Alloca' 'Family']
แย่ละครับ บอกเลยว่าอ่านไม่รู้เรื่องเลย มันให้มาเป็นสัญลักษณ์อะไรก็ไม่รู้ แน่นอนว่าเจอแบบนี้ต้องไปดู Data Description ว่ามันให้อะไรมา
มาถึงตรงนี้ผมตัดสินใจแล้วว่าจะเปลี่ยนไอ้ตัวแปรรหัสพวกนี้ไปเป็นคำอธิบายไปเลย เพื่อความเข้าใจที่ง่ายขึ้น(ของตัวผมเอง)
บรรทัดนี้ซ้อมก่อนครับ กลัวทำพลาด ข้ามไปได้เลย
import re
import pandas as pd
def extract_values_to_dataframe(content):
# ใช้ Regex ในการช่วยอ่าน Pattern
pattern = r'(?P<Label>^[^:]+):\s*(?P<Description>[^\n]+)(?P<Values>(?:\n\t\d+\t[^\n]+)+)'
# เอาเฉพาะตัวที่มัน Match กับ Pattern
matches = re.finditer(pattern, content, re.MULTILINE)
# สร้าง dictionary มาเก็บไว้
data_dict = {}
for match in matches:
label = match.group("Label").strip()
values_section = match.group("Values")
value_pairs = re.findall(r'\t(\d+)\t([^\n]+)', values_section)
# ยัดข้อมูลที่ตัดมาได้ไปไว้ใน Dict
data_dict[label] = {int(key): value for key, value in value_pairs}
# ยัดข้อมูลใน Dict ไปไว้ใน DataFrame อีกที
result_df = pd.DataFrame([data_dict]).T
result_df.columns = ["Enumerated Values"]
return result_df
# ทดสอบ
mock_content = """
Color: Describes the color
1 Red
2 Blue
3 Green
Size: Indicates size
1 Small
2 Medium
3 Large
"""
result_df = extract_values_to_dataframe(mock_content)
print(result_df)
mapping_dict = result_df["Enumerated Values"].to_dict()
b = {
"Color": [1, 2, 3, 1, 3],
"Size": [2, 3, 1, 2, 3]
}
a = pd.DataFrame(b)
# ลองเปลี่ยน
a['Color'] = a['Color'].map(mapping_dict['Color'])
a['Size'] = a['Size'].map(mapping_dict['Size'])
print(a)
Enumerated Values
Color {1: 'Red', 2: 'Blue', 3: 'Green'}
Size {1: 'Small', 2: 'Medium', 3: 'Large'}
Color Size
0 Red Medium
1 Blue Large
2 Green Small
3 Red Medium
4 Green Large
ทีนี้ลองมาดูของจริงกันบ้าง (อันนี้เปลี่ยนวิธีอ่านนิดนึงครับ สังเกตุว่าใน txt.file มันใช้ Tap ขั้น) และจุดที่น่าสังเกตุคือตัวแปรที่เป็น Categorial มันจะมี Subkey ด้านใน ในขณะที่ตัวแปรประเภท Numerical มันจะมีคำบรรยายแค่บรรทัดเดียว
ตัวอย่าง
MSZoning: Identifies the general zoning classification of the sale.
A Agriculture
C Commercial
FV Floating Village Residential
I Industrial
RH Residential High Density
RL Residential Low Density
RP Residential Low Density Park
RM Residential Medium Density
LotFrontage: Linear feet of street connected to property
LotArea: Lot size in square feet
Street: Type of road access to property
Grvl Gravel
Pave Paved
ทีนี้มาพูดถึงวิธีการตัดกันบ้าง เราจะใช้ Dictionary 2 ชุดคือ temp_dict และ property_dict โดยภาพที่วางไว้คือเราจะตัดค่าต่างๆ ยังใส่ temp_dict ก่อน จากนั้นถ้าเจอ key ใหม่(วนครบทุก sub_key ใน key หลักแล้ว) เราจะยัดก้อนที่เสร็จแล้วใส่ property_dict ทำแบบนี้ไปเรื่อยๆ จนเสร็จ
คำถามคือตอนนี้เรามองหาอะไร ? จาก Pattern ที่ให้มามันจะมี 2 Pattern ที่ไว้แยก Key กับ sub_keys โดย
Key:Description
sub_keys: Value
ทีนี้มาดูแนวคิดการตัดกันต่อ
1.ขั้นตอนคือเรามองหาเครื่องหมาย : เพราะ Pattern มันจะเป็น Key:Description
2.วิธีการเช็คว่ามันเป็น Sub_key จริงไหมทำได้โดยการใช้
len(stripped_line.split("\t")) == 2:
เพราะถ้าเรา split ตัวที่ขั้นด้วย Tap มันจะต้องแยกออกมาได้ 2 ตัว
def extract_enumerations_from_file(file_path):
# สร้าง temp_dict ขึ้นมา
temp_dict = {}
# สร้าง current_key กับ current_desc ขึ้นมาแบบเปล่าๆ ก่อน
current_key = None
current_desc = None
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
for line in lines:
stripped_line = line.strip()
# เงื่อนไขนี้เช็คว่าบรรทัดที่มี colon(;) เป็นบรรทัดหลักใช่หรือไม่ ?
if ':' in stripped_line:
# ถ้าผ่านเงื่อนไขข้างบนมา เราก็ต้องมาเช็คต่อว่ามี Sub_keys ไหม ถ้ามีเราถึงจะเก็บเค้าไว้ใน Property_dict ถ้าไม่มีก็โยนทิ้ง ไม่บันทึก
if current_key and temp_dict[current_key]["sub_keys"]:
property_dict[current_key] = temp_dict[current_key]
# คือถ้าผ่านเงื่อนไขนี้ได้ แปลว่ายังไงมันก็ต้องเก็บ current_key, current_desc ไว้ก่อน
#โดยเก็บไว้ด้วย {"desc": current_desc.strip(), "sub_keys": {}} ว่าง sub_keys ไว้รอเติม
current_key, current_desc = stripped_line.split(":")
temp_dict[current_key] = {"desc": current_desc.strip(), "sub_keys": {}}
# ถ้ามาเงื่อนไขนี้แปลว่ากำลังเช็คว่าบรรทัดที่อ่านอยู่เป็น sub_key ใช่หรือไม่ ? ถ้าใช่เราจะเก็บเค้าไว้ใน Temp Dict
elif current_key and len(stripped_line.split("\t")) == 2:
sub_key, value = stripped_line.split("\t")
temp_dict[current_key]["sub_keys"][sub_key.strip()] = value.strip()
# อันนี้เผื่อไว้สำหรับ Key สุดท้ายถ้ามันมี sub_keys
if current_key and temp_dict[current_key]["sub_keys"]:
property_dict[current_key] = temp_dict[current_key]
return property_dict
# สร้าง property_dict ขึ้นมา
property_dict = {}
#อันนี้ทำ fucntion สำหรับอ่าน dictionary เฉยๆ
def print_enum_dict(dictionary):
for key, sub_dict in dictionary.items():
print(f"{key}:")
for sub_key, value in sub_dict.items():
print(f" {sub_key} -> {value}")
print()
# ใช้งาน
file_path = '/content/drive/MyDrive/EDA House Price with Python/data_description (1).txt'
enum_dict_2 = extract_enumerations_from_file(file_path)
print_enum_dict(enum_dict_2)
MSSubClass:
desc -> Identifies the type of dwelling involved in the sale.
sub_keys -> {'20': '1-STORY 1946 & NEWER ALL STYLES', '30': '1-STORY 1945 & OLDER', '40': '1-STORY W/FINISHED ATTIC ALL AGES', '45': '1-1/2 STORY - UNFINISHED ALL AGES', '50': '1-1/2 STORY FINISHED ALL AGES', '60': '2-STORY 1946 & NEWER', '70': '2-STORY 1945 & OLDER', '75': '2-1/2 STORY ALL AGES', '80': 'SPLIT OR MULTI-LEVEL', '85': 'SPLIT FOYER', '90': 'DUPLEX - ALL STYLES AND AGES', '120': '1-STORY PUD (Planned Unit Development) - 1946 & NEWER', '150': '1-1/2 STORY PUD - ALL AGES', '160': '2-STORY PUD - 1946 & NEWER', '180': 'PUD - MULTILEVEL - INCL SPLIT LEV/FOYER', '190': '2 FAMILY CONVERSION - ALL STYLES AND AGES'}
MSZoning:
desc -> Identifies the general zoning classification of the sale.
sub_keys -> {'FV': 'Floating Village Residential', 'RH': 'Residential High Density', 'RL': 'Residential Low Density', 'RP': 'Residential Low Density Park', 'RM': 'Residential Medium Density'}
Street:
desc -> Type of road access to property
sub_keys -> {'Grvl': 'Gravel', 'Pave': 'Paved'}
Alley:
desc -> Type of alley access to property
sub_keys -> {'Grvl': 'Gravel', 'Pave': 'Paved', 'NA': 'No alley access'}
LotShape:
desc -> General shape of property
sub_keys -> {'Reg': 'Regular', 'IR1': 'Slightly irregular', 'IR2': 'Moderately Irregular', 'IR3': 'Irregular'}
LandContour:
desc -> Flatness of the property
sub_keys -> {'Lvl': 'Near Flat/Level', 'Bnk': 'Banked - Quick and significant rise from street grade to building', 'HLS': 'Hillside - Significant slope from side to side', 'Low': 'Depression'}
Utilities:
desc -> Type of utilities available
sub_keys -> {'AllPub': 'All public Utilities (E,G,W,& S)', 'NoSewr': 'Electricity, Gas, and Water (Septic Tank)', 'NoSeWa': 'Electricity and Gas Only', 'ELO': 'Electricity only'}
LotConfig:
desc -> Lot configuration
sub_keys -> {'Inside': 'Inside lot', 'Corner': 'Corner lot', 'CulDSac': 'Cul-de-sac', 'FR2': 'Frontage on 2 sides of property', 'FR3': 'Frontage on 3 sides of property'}
LandSlope:
desc -> Slope of property
sub_keys -> {'Gtl': 'Gentle slope', 'Mod': 'Moderate Slope', 'Sev': 'Severe Slope'}
Neighborhood:
desc -> Physical locations within Ames city limits
sub_keys -> {'Blmngtn': 'Bloomington Heights', 'Blueste': 'Bluestem', 'BrDale': 'Briardale', 'BrkSide': 'Brookside', 'ClearCr': 'Clear Creek', 'CollgCr': 'College Creek', 'Crawfor': 'Crawford', 'Edwards': 'Edwards', 'Gilbert': 'Gilbert', 'IDOTRR': 'Iowa DOT and Rail Road', 'MeadowV': 'Meadow Village', 'Mitchel': 'Mitchell', 'Names': 'North Ames', 'NoRidge': 'Northridge', 'NPkVill': 'Northpark Villa', 'NridgHt': 'Northridge Heights', 'NWAmes': 'Northwest Ames', 'OldTown': 'Old Town', 'SWISU': 'South & West of Iowa State University', 'Sawyer': 'Sawyer', 'SawyerW': 'Sawyer West', 'Somerst': 'Somerset', 'StoneBr': 'Stone Brook', 'Timber': 'Timberland', 'Veenker': 'Veenker'}
Condition1:
desc -> Proximity to various conditions
sub_keys -> {'Artery': 'Adjacent to arterial street', 'Feedr': 'Adjacent to feeder street', 'Norm': 'Normal', 'RRNn': "Within 200' of North-South Railroad", 'RRAn': 'Adjacent to North-South Railroad', 'PosN': 'Near positive off-site feature--park, greenbelt, etc.', 'PosA': 'Adjacent to postive off-site feature', 'RRNe': "Within 200' of East-West Railroad", 'RRAe': 'Adjacent to East-West Railroad'}
Condition2:
desc -> Proximity to various conditions (if more than one is present)
sub_keys -> {'Artery': 'Adjacent to arterial street', 'Feedr': 'Adjacent to feeder street', 'Norm': 'Normal', 'RRNn': "Within 200' of North-South Railroad", 'RRAn': 'Adjacent to North-South Railroad', 'PosN': 'Near positive off-site feature--park, greenbelt, etc.', 'PosA': 'Adjacent to postive off-site feature', 'RRNe': "Within 200' of East-West Railroad", 'RRAe': 'Adjacent to East-West Railroad'}
BldgType:
desc -> Type of dwelling
sub_keys -> {'1Fam': 'Single-family Detached', '2FmCon': 'Two-family Conversion; originally built as one-family dwelling', 'Duplx': 'Duplex', 'TwnhsE': 'Townhouse End Unit', 'TwnhsI': 'Townhouse Inside Unit'}
HouseStyle:
desc -> Style of dwelling
sub_keys -> {'1Story': 'One story'}
1.5Unf One and one-half story:
desc -> 2nd level unfinished
sub_keys -> {'2Story': 'Two story'}
2.5Unf Two and one-half story:
desc -> 2nd level unfinished
sub_keys -> {'SFoyer': 'Split Foyer', 'SLvl': 'Split Level'}
OverallQual:
desc -> Rates the overall material and finish of the house
sub_keys -> {'10': 'Very Excellent', '9': 'Excellent', '8': 'Very Good', '7': 'Good', '6': 'Above Average', '5': 'Average', '4': 'Below Average', '3': 'Fair', '2': 'Poor', '1': 'Very Poor'}
OverallCond:
desc -> Rates the overall condition of the house
sub_keys -> {'10': 'Very Excellent', '9': 'Excellent', '8': 'Very Good', '7': 'Good', '6': 'Above Average', '5': 'Average', '4': 'Below Average', '3': 'Fair', '2': 'Poor', '1': 'Very Poor'}
RoofStyle:
desc -> Type of roof
sub_keys -> {'Flat': 'Flat', 'Gable': 'Gable', 'Gambrel': 'Gabrel (Barn)', 'Hip': 'Hip', 'Mansard': 'Mansard', 'Shed': 'Shed'}
RoofMatl:
desc -> Roof material
sub_keys -> {'ClyTile': 'Clay or Tile', 'CompShg': 'Standard (Composite) Shingle', 'Membran': 'Membrane', 'Metal': 'Metal', 'Roll': 'Roll', 'Tar&Grv': 'Gravel & Tar', 'WdShake': 'Wood Shakes', 'WdShngl': 'Wood Shingles'}
Exterior1st:
desc -> Exterior covering on house
sub_keys -> {'AsbShng': 'Asbestos Shingles', 'AsphShn': 'Asphalt Shingles', 'BrkComm': 'Brick Common', 'BrkFace': 'Brick Face', 'CBlock': 'Cinder Block', 'CemntBd': 'Cement Board', 'HdBoard': 'Hard Board', 'ImStucc': 'Imitation Stucco', 'MetalSd': 'Metal Siding', 'Other': 'Other', 'Plywood': 'Plywood', 'PreCast': 'PreCast', 'Stone': 'Stone', 'Stucco': 'Stucco', 'VinylSd': 'Vinyl Siding', 'Wd Sdng': 'Wood Siding', 'WdShing': 'Wood Shingles'}
Exterior2nd:
desc -> Exterior covering on house (if more than one material)
sub_keys -> {'AsbShng': 'Asbestos Shingles', 'AsphShn': 'Asphalt Shingles', 'BrkComm': 'Brick Common', 'BrkFace': 'Brick Face', 'CBlock': 'Cinder Block', 'CemntBd': 'Cement Board', 'HdBoard': 'Hard Board', 'ImStucc': 'Imitation Stucco', 'MetalSd': 'Metal Siding', 'Other': 'Other', 'Plywood': 'Plywood', 'PreCast': 'PreCast', 'Stone': 'Stone', 'Stucco': 'Stucco', 'VinylSd': 'Vinyl Siding', 'Wd Sdng': 'Wood Siding', 'WdShing': 'Wood Shingles'}
MasVnrType:
desc -> Masonry veneer type
sub_keys -> {'BrkCmn': 'Brick Common', 'BrkFace': 'Brick Face', 'CBlock': 'Cinder Block', 'None': 'None', 'Stone': 'Stone'}
ExterQual:
desc -> Evaluates the quality of the material on the exterior
sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Average/Typical', 'Fa': 'Fair', 'Po': 'Poor'}
ExterCond:
desc -> Evaluates the present condition of the material on the exterior
sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Average/Typical', 'Fa': 'Fair', 'Po': 'Poor'}
Foundation:
desc -> Type of foundation
sub_keys -> {'BrkTil': 'Brick & Tile', 'CBlock': 'Cinder Block', 'PConc': 'Poured Contrete', 'Slab': 'Slab', 'Stone': 'Stone', 'Wood': 'Wood'}
BsmtQual:
desc -> Evaluates the height of the basement
sub_keys -> {'Ex': 'Excellent (100+ inches)', 'Gd': 'Good (90-99 inches)', 'TA': 'Typical (80-89 inches)', 'Fa': 'Fair (70-79 inches)', 'Po': 'Poor (<70 inches', 'NA': 'No Basement'}
BsmtCond:
desc -> Evaluates the general condition of the basement
sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Typical - slight dampness allowed', 'Fa': 'Fair - dampness or some cracking or settling', 'Po': 'Poor - Severe cracking, settling, or wetness', 'NA': 'No Basement'}
BsmtExposure:
desc -> Refers to walkout or garden level walls
sub_keys -> {'Gd': 'Good Exposure', 'Av': 'Average Exposure (split levels or foyers typically score average or above)', 'Mn': 'Mimimum Exposure', 'No': 'No Exposure', 'NA': 'No Basement'}
BsmtFinType1:
desc -> Rating of basement finished area
sub_keys -> {'GLQ': 'Good Living Quarters', 'ALQ': 'Average Living Quarters', 'BLQ': 'Below Average Living Quarters', 'Rec': 'Average Rec Room', 'LwQ': 'Low Quality', 'Unf': 'Unfinshed', 'NA': 'No Basement'}
BsmtFinType2:
desc -> Rating of basement finished area (if multiple types)
sub_keys -> {'GLQ': 'Good Living Quarters', 'ALQ': 'Average Living Quarters', 'BLQ': 'Below Average Living Quarters', 'Rec': 'Average Rec Room', 'LwQ': 'Low Quality', 'Unf': 'Unfinshed', 'NA': 'No Basement'}
Heating:
desc -> Type of heating
sub_keys -> {'Floor': 'Floor Furnace', 'GasA': 'Gas forced warm air furnace', 'GasW': 'Gas hot water or steam heat', 'Grav': 'Gravity furnace', 'OthW': 'Hot water or steam heat other than gas', 'Wall': 'Wall furnace'}
HeatingQC:
desc -> Heating quality and condition
sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Average/Typical', 'Fa': 'Fair', 'Po': 'Poor'}
CentralAir:
desc -> Central air conditioning
sub_keys -> {'N': 'No', 'Y': 'Yes'}
Electrical:
desc -> Electrical system
sub_keys -> {'SBrkr': 'Standard Circuit Breakers & Romex', 'FuseA': 'Fuse Box over 60 AMP and all Romex wiring (Average)', 'FuseF': '60 AMP Fuse Box and mostly Romex wiring (Fair)', 'FuseP': '60 AMP Fuse Box and mostly knob & tube wiring (poor)', 'Mix': 'Mixed'}
KitchenQual:
desc -> Kitchen quality
sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Typical/Average', 'Fa': 'Fair', 'Po': 'Poor'}
Functional:
desc -> Home functionality (Assume typical unless deductions are warranted)
sub_keys -> {'Typ': 'Typical Functionality', 'Min1': 'Minor Deductions 1', 'Min2': 'Minor Deductions 2', 'Mod': 'Moderate Deductions', 'Maj1': 'Major Deductions 1', 'Maj2': 'Major Deductions 2', 'Sev': 'Severely Damaged', 'Sal': 'Salvage only'}
FireplaceQu:
desc -> Fireplace quality
sub_keys -> {'Ex': 'Excellent - Exceptional Masonry Fireplace', 'Gd': 'Good - Masonry Fireplace in main level', 'TA': 'Average - Prefabricated Fireplace in main living area or Masonry Fireplace in basement', 'Fa': 'Fair - Prefabricated Fireplace in basement', 'Po': 'Poor - Ben Franklin Stove', 'NA': 'No Fireplace'}
GarageType:
desc -> Garage location
sub_keys -> {'2Types': 'More than one type of garage', 'Attchd': 'Attached to home', 'Basment': 'Basement Garage', 'BuiltIn': 'Built-In (Garage part of house - typically has room above garage)', 'CarPort': 'Car Port', 'Detchd': 'Detached from home', 'NA': 'No Garage'}
GarageFinish:
desc -> Interior finish of the garage
sub_keys -> {'Fin': 'Finished', 'RFn': 'Rough Finished', 'Unf': 'Unfinished', 'NA': 'No Garage'}
GarageQual:
desc -> Garage quality
sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Typical/Average', 'Fa': 'Fair', 'Po': 'Poor', 'NA': 'No Garage'}
GarageCond:
desc -> Garage condition
sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Typical/Average', 'Fa': 'Fair', 'Po': 'Poor', 'NA': 'No Garage'}
PavedDrive:
desc -> Paved driveway
sub_keys -> {'Y': 'Paved', 'P': 'Partial Pavement', 'N': 'Dirt/Gravel'}
PoolQC:
desc -> Pool quality
sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Average/Typical', 'Fa': 'Fair', 'NA': 'No Pool'}
Fence:
desc -> Fence quality
sub_keys -> {'GdPrv': 'Good Privacy', 'MnPrv': 'Minimum Privacy', 'GdWo': 'Good Wood', 'MnWw': 'Minimum Wood/Wire', 'NA': 'No Fence'}
MiscFeature:
desc -> Miscellaneous feature not covered in other categories
sub_keys -> {'Elev': 'Elevator', 'Gar2': '2nd Garage (if not described in garage section)', 'Othr': 'Other', 'Shed': 'Shed (over 100 SF)', 'TenC': 'Tennis Court', 'NA': 'None'}
SaleType:
desc -> Type of sale
sub_keys -> {'WD': 'Warranty Deed - Conventional', 'CWD': 'Warranty Deed - Cash', 'VWD': 'Warranty Deed - VA Loan', 'New': 'Home just constructed and sold', 'COD': 'Court Officer Deed/Estate', 'Con': 'Contract 15% Down payment regular terms', 'ConLw': 'Contract Low Down payment and low interest', 'ConLI': 'Contract Low Interest', 'ConLD': 'Contract Low Down', 'Oth': 'Other'}
SaleCondition:
desc -> Condition of sale
sub_keys -> {'Normal': 'Normal Sale', 'Abnorml': 'Abnormal Sale - trade, foreclosure, short sale', 'AdjLand': 'Adjoining Land Purchase', 'Alloca': 'Allocation - two linked properties with separate deeds, typically condo with a garage unit', 'Family': 'Sale between family members', 'Partial': 'Home was not completed when last assessed (associated with New Homes)'}
เดี๋ยวถึงตรงนี้ขอหยุดขั้นตอนแปลงไว้ตรงนี้ก่อน พอดีลองอ่าน Description แล้วพบว่าตัว NaN ในข้อมูลชุดนี้หมายถึง "ไม่มี"
โดยปกติแล้วข้อมูลที่เป็น Null Value มันเกิดจากการลืมเติม หรือข้อมูลหายไป แต่กับข้อมูลชุดนี้ ซึ่งเกี่ยวกับอสังหาริมทรัพย์ คำว่าไม่มีมันก็คือไม่มี
ถ้ายกตัวอย่างง่ายๆ เช่น PoolQC = NA มันกำลังบอกว่าบ้านหลังนี้ไม่มีสระว่ายน้ำ ไม่ได้แปลว่าลืมเติมแต่อย่างใด
ทั้งนี้การจัดการกับข้อมูลที่หายไป หลักๆ จะต้มยำทำแกงบ่อยๆ ได้ดังนี้
Numerical
Categorial
ทั้งนี้ เพื่อให้เห็นภาพมากขึ้น เราจะทำการนับ Missing Value แล้วเอามา Plot เพื่อความชัดเจน
missing = df.isnull().sum()
missing = missing[missing > 0]
missing.sort_values(inplace=True)
missing.plot.bar()
<Axes: >
โอเค เดี๋ยวแยก Quantitative กับ Qualitative ดีกว่า
quantitative = df.select_dtypes(include=(['int64', 'float64']))
quantitative_missing = quantitative.isnull().sum()
quantitative_missing = quantitative_missing[quantitative_missing > 0]
quantitative_missing.sort_values(inplace=True)
quantitative_missing.plot.bar()
<Axes: >
เราต้องมาเช็คอีกทีว่า GarageYrBlt กับ MasVnrArea ที่หายไปมันเท่ากับ GarageType,MasVnrType หรือไม่ ถ้ามันเท่ากันแปลว่าช่องที่หายไป ควรจะ
print(df['GarageType'].isnull().sum() == df['GarageYrBlt'].isnull().sum())
print(df['MasVnrArea'].isnull().sum() == df['MasVnrType'].isnull().sum())
True True
df['GarageYrBlt']=df['GarageYrBlt'].fillna(0)
df['MasVnrArea']=df['MasVnrArea'].fillna(0)
df['LotFrontage']=df['LotFrontage'].fillna(0)
เช็คอีกทีว่ามีตัวแปรไหนหายไปไหม ?
missing_value_found = False
for col in quantitative:
if df[col].isnull().sum() > 0:
print(col)
missing_value_found = True
if not missing_value_found:
print("No Missing Value")
No Missing Value
ทำแบบเดิมกับตัวแปร Qualitative อีกรอบ
qualitative = df.select_dtypes(include=['object'])
qualitative_missing = qualitative.isnull().sum()
qualitative_missing = qualitative_missing[qualitative_missing > 0]
qualitative_missing.sort_values(inplace=True)
qualitative_missing.plot.bar()
<Axes: >
ยัด NA เข้าไปในทุกช่องว่าง แต่สำหรับ MasVnrType มันจะมีปัญหานิดหน่อยเพราะมีทั้ง NA และ None เลยต้องเปลี่ยนอีกที
for col in qualitative:
df[col]=df[col].fillna('NA')
df.head()
df['MasVnrType']=df['MasVnrType'].replace(['NA'],'None')
df['MasVnrType'].unique()
array(['BrkFace', 'None', 'Stone', 'BrkCmn'], dtype=object)
กลับมาที่ Dictionary ของเรา ด้วยความที่ชุดข้อมูลที่เราตัดมามันเป็น Nested Dictionary และเอาไปใช้ยาก ดังนั้นการตัดให้มันเป็นบรรทัดเดียวแบบ Dictionary ทั่วไปที่บันทึก Key กับ Value น่าจะเป็นตัวเลือกที่ดีกว่า
flattened_dict = {}
for key, values in enum_dict_2.items():
for sub_key, sub_value in values['sub_keys'].items():
flattened_key = f"{key}_{sub_key}"
flattened_dict[flattened_key] = sub_value
print(flattened_dict)
{'MSSubClass_20': '1-STORY 1946 & NEWER ALL STYLES', 'MSSubClass_30': '1-STORY 1945 & OLDER', 'MSSubClass_40': '1-STORY W/FINISHED ATTIC ALL AGES', 'MSSubClass_45': '1-1/2 STORY - UNFINISHED ALL AGES', 'MSSubClass_50': '1-1/2 STORY FINISHED ALL AGES', 'MSSubClass_60': '2-STORY 1946 & NEWER', 'MSSubClass_70': '2-STORY 1945 & OLDER', 'MSSubClass_75': '2-1/2 STORY ALL AGES', 'MSSubClass_80': 'SPLIT OR MULTI-LEVEL', 'MSSubClass_85': 'SPLIT FOYER', 'MSSubClass_90': 'DUPLEX - ALL STYLES AND AGES', 'MSSubClass_120': '1-STORY PUD (Planned Unit Development) - 1946 & NEWER', 'MSSubClass_150': '1-1/2 STORY PUD - ALL AGES', 'MSSubClass_160': '2-STORY PUD - 1946 & NEWER', 'MSSubClass_180': 'PUD - MULTILEVEL - INCL SPLIT LEV/FOYER', 'MSSubClass_190': '2 FAMILY CONVERSION - ALL STYLES AND AGES', 'MSZoning_FV': 'Floating Village Residential', 'MSZoning_RH': 'Residential High Density', 'MSZoning_RL': 'Residential Low Density', 'MSZoning_RP': 'Residential Low Density Park', 'MSZoning_RM': 'Residential Medium Density', 'Street_Grvl': 'Gravel', 'Street_Pave': 'Paved', 'Alley_Grvl': 'Gravel', 'Alley_Pave': 'Paved', 'Alley_NA': 'No alley access', 'LotShape_Reg': 'Regular', 'LotShape_IR1': 'Slightly irregular', 'LotShape_IR2': 'Moderately Irregular', 'LotShape_IR3': 'Irregular', 'LandContour_Lvl': 'Near Flat/Level', 'LandContour_Bnk': 'Banked - Quick and significant rise from street grade to building', 'LandContour_HLS': 'Hillside - Significant slope from side to side', 'LandContour_Low': 'Depression', 'Utilities_AllPub': 'All public Utilities (E,G,W,& S)', 'Utilities_NoSewr': 'Electricity, Gas, and Water (Septic Tank)', 'Utilities_NoSeWa': 'Electricity and Gas Only', 'Utilities_ELO': 'Electricity only', 'LotConfig_Inside': 'Inside lot', 'LotConfig_Corner': 'Corner lot', 'LotConfig_CulDSac': 'Cul-de-sac', 'LotConfig_FR2': 'Frontage on 2 sides of property', 'LotConfig_FR3': 'Frontage on 3 sides of property', 'LandSlope_Gtl': 'Gentle slope', 'LandSlope_Mod': 'Moderate Slope', 'LandSlope_Sev': 'Severe Slope', 'Neighborhood_Blmngtn': 'Bloomington Heights', 'Neighborhood_Blueste': 'Bluestem', 'Neighborhood_BrDale': 'Briardale', 'Neighborhood_BrkSide': 'Brookside', 'Neighborhood_ClearCr': 'Clear Creek', 'Neighborhood_CollgCr': 'College Creek', 'Neighborhood_Crawfor': 'Crawford', 'Neighborhood_Edwards': 'Edwards', 'Neighborhood_Gilbert': 'Gilbert', 'Neighborhood_IDOTRR': 'Iowa DOT and Rail Road', 'Neighborhood_MeadowV': 'Meadow Village', 'Neighborhood_Mitchel': 'Mitchell', 'Neighborhood_Names': 'North Ames', 'Neighborhood_NoRidge': 'Northridge', 'Neighborhood_NPkVill': 'Northpark Villa', 'Neighborhood_NridgHt': 'Northridge Heights', 'Neighborhood_NWAmes': 'Northwest Ames', 'Neighborhood_OldTown': 'Old Town', 'Neighborhood_SWISU': 'South & West of Iowa State University', 'Neighborhood_Sawyer': 'Sawyer', 'Neighborhood_SawyerW': 'Sawyer West', 'Neighborhood_Somerst': 'Somerset', 'Neighborhood_StoneBr': 'Stone Brook', 'Neighborhood_Timber': 'Timberland', 'Neighborhood_Veenker': 'Veenker', 'Condition1_Artery': 'Adjacent to arterial street', 'Condition1_Feedr': 'Adjacent to feeder street', 'Condition1_Norm': 'Normal', 'Condition1_RRNn': "Within 200' of North-South Railroad", 'Condition1_RRAn': 'Adjacent to North-South Railroad', 'Condition1_PosN': 'Near positive off-site feature--park, greenbelt, etc.', 'Condition1_PosA': 'Adjacent to postive off-site feature', 'Condition1_RRNe': "Within 200' of East-West Railroad", 'Condition1_RRAe': 'Adjacent to East-West Railroad', 'Condition2_Artery': 'Adjacent to arterial street', 'Condition2_Feedr': 'Adjacent to feeder street', 'Condition2_Norm': 'Normal', 'Condition2_RRNn': "Within 200' of North-South Railroad", 'Condition2_RRAn': 'Adjacent to North-South Railroad', 'Condition2_PosN': 'Near positive off-site feature--park, greenbelt, etc.', 'Condition2_PosA': 'Adjacent to postive off-site feature', 'Condition2_RRNe': "Within 200' of East-West Railroad", 'Condition2_RRAe': 'Adjacent to East-West Railroad', 'BldgType_1Fam': 'Single-family Detached', 'BldgType_2FmCon': 'Two-family Conversion; originally built as one-family dwelling', 'BldgType_Duplx': 'Duplex', 'BldgType_TwnhsE': 'Townhouse End Unit', 'BldgType_TwnhsI': 'Townhouse Inside Unit', 'HouseStyle_1Story': 'One story', '1.5Unf\tOne and one-half story_2Story': 'Two story', '2.5Unf\tTwo and one-half story_SFoyer': 'Split Foyer', '2.5Unf\tTwo and one-half story_SLvl': 'Split Level', 'OverallQual_10': 'Very Excellent', 'OverallQual_9': 'Excellent', 'OverallQual_8': 'Very Good', 'OverallQual_7': 'Good', 'OverallQual_6': 'Above Average', 'OverallQual_5': 'Average', 'OverallQual_4': 'Below Average', 'OverallQual_3': 'Fair', 'OverallQual_2': 'Poor', 'OverallQual_1': 'Very Poor', 'OverallCond_10': 'Very Excellent', 'OverallCond_9': 'Excellent', 'OverallCond_8': 'Very Good', 'OverallCond_7': 'Good', 'OverallCond_6': 'Above Average', 'OverallCond_5': 'Average', 'OverallCond_4': 'Below Average', 'OverallCond_3': 'Fair', 'OverallCond_2': 'Poor', 'OverallCond_1': 'Very Poor', 'RoofStyle_Flat': 'Flat', 'RoofStyle_Gable': 'Gable', 'RoofStyle_Gambrel': 'Gabrel (Barn)', 'RoofStyle_Hip': 'Hip', 'RoofStyle_Mansard': 'Mansard', 'RoofStyle_Shed': 'Shed', 'RoofMatl_ClyTile': 'Clay or Tile', 'RoofMatl_CompShg': 'Standard (Composite) Shingle', 'RoofMatl_Membran': 'Membrane', 'RoofMatl_Metal': 'Metal', 'RoofMatl_Roll': 'Roll', 'RoofMatl_Tar&Grv': 'Gravel & Tar', 'RoofMatl_WdShake': 'Wood Shakes', 'RoofMatl_WdShngl': 'Wood Shingles', 'Exterior1st_AsbShng': 'Asbestos Shingles', 'Exterior1st_AsphShn': 'Asphalt Shingles', 'Exterior1st_BrkComm': 'Brick Common', 'Exterior1st_BrkFace': 'Brick Face', 'Exterior1st_CBlock': 'Cinder Block', 'Exterior1st_CemntBd': 'Cement Board', 'Exterior1st_HdBoard': 'Hard Board', 'Exterior1st_ImStucc': 'Imitation Stucco', 'Exterior1st_MetalSd': 'Metal Siding', 'Exterior1st_Other': 'Other', 'Exterior1st_Plywood': 'Plywood', 'Exterior1st_PreCast': 'PreCast', 'Exterior1st_Stone': 'Stone', 'Exterior1st_Stucco': 'Stucco', 'Exterior1st_VinylSd': 'Vinyl Siding', 'Exterior1st_Wd Sdng': 'Wood Siding', 'Exterior1st_WdShing': 'Wood Shingles', 'Exterior2nd_AsbShng': 'Asbestos Shingles', 'Exterior2nd_AsphShn': 'Asphalt Shingles', 'Exterior2nd_BrkComm': 'Brick Common', 'Exterior2nd_BrkFace': 'Brick Face', 'Exterior2nd_CBlock': 'Cinder Block', 'Exterior2nd_CemntBd': 'Cement Board', 'Exterior2nd_HdBoard': 'Hard Board', 'Exterior2nd_ImStucc': 'Imitation Stucco', 'Exterior2nd_MetalSd': 'Metal Siding', 'Exterior2nd_Other': 'Other', 'Exterior2nd_Plywood': 'Plywood', 'Exterior2nd_PreCast': 'PreCast', 'Exterior2nd_Stone': 'Stone', 'Exterior2nd_Stucco': 'Stucco', 'Exterior2nd_VinylSd': 'Vinyl Siding', 'Exterior2nd_Wd Sdng': 'Wood Siding', 'Exterior2nd_WdShing': 'Wood Shingles', 'MasVnrType_BrkCmn': 'Brick Common', 'MasVnrType_BrkFace': 'Brick Face', 'MasVnrType_CBlock': 'Cinder Block', 'MasVnrType_None': 'None', 'MasVnrType_Stone': 'Stone', 'ExterQual_Ex': 'Excellent', 'ExterQual_Gd': 'Good', 'ExterQual_TA': 'Average/Typical', 'ExterQual_Fa': 'Fair', 'ExterQual_Po': 'Poor', 'ExterCond_Ex': 'Excellent', 'ExterCond_Gd': 'Good', 'ExterCond_TA': 'Average/Typical', 'ExterCond_Fa': 'Fair', 'ExterCond_Po': 'Poor', 'Foundation_BrkTil': 'Brick & Tile', 'Foundation_CBlock': 'Cinder Block', 'Foundation_PConc': 'Poured Contrete', 'Foundation_Slab': 'Slab', 'Foundation_Stone': 'Stone', 'Foundation_Wood': 'Wood', 'BsmtQual_Ex': 'Excellent (100+ inches)', 'BsmtQual_Gd': 'Good (90-99 inches)', 'BsmtQual_TA': 'Typical (80-89 inches)', 'BsmtQual_Fa': 'Fair (70-79 inches)', 'BsmtQual_Po': 'Poor (<70 inches', 'BsmtQual_NA': 'No Basement', 'BsmtCond_Ex': 'Excellent', 'BsmtCond_Gd': 'Good', 'BsmtCond_TA': 'Typical - slight dampness allowed', 'BsmtCond_Fa': 'Fair - dampness or some cracking or settling', 'BsmtCond_Po': 'Poor - Severe cracking, settling, or wetness', 'BsmtCond_NA': 'No Basement', 'BsmtExposure_Gd': 'Good Exposure', 'BsmtExposure_Av': 'Average Exposure (split levels or foyers typically score average or above)', 'BsmtExposure_Mn': 'Mimimum Exposure', 'BsmtExposure_No': 'No Exposure', 'BsmtExposure_NA': 'No Basement', 'BsmtFinType1_GLQ': 'Good Living Quarters', 'BsmtFinType1_ALQ': 'Average Living Quarters', 'BsmtFinType1_BLQ': 'Below Average Living Quarters', 'BsmtFinType1_Rec': 'Average Rec Room', 'BsmtFinType1_LwQ': 'Low Quality', 'BsmtFinType1_Unf': 'Unfinshed', 'BsmtFinType1_NA': 'No Basement', 'BsmtFinType2_GLQ': 'Good Living Quarters', 'BsmtFinType2_ALQ': 'Average Living Quarters', 'BsmtFinType2_BLQ': 'Below Average Living Quarters', 'BsmtFinType2_Rec': 'Average Rec Room', 'BsmtFinType2_LwQ': 'Low Quality', 'BsmtFinType2_Unf': 'Unfinshed', 'BsmtFinType2_NA': 'No Basement', 'Heating_Floor': 'Floor Furnace', 'Heating_GasA': 'Gas forced warm air furnace', 'Heating_GasW': 'Gas hot water or steam heat', 'Heating_Grav': 'Gravity furnace', 'Heating_OthW': 'Hot water or steam heat other than gas', 'Heating_Wall': 'Wall furnace', 'HeatingQC_Ex': 'Excellent', 'HeatingQC_Gd': 'Good', 'HeatingQC_TA': 'Average/Typical', 'HeatingQC_Fa': 'Fair', 'HeatingQC_Po': 'Poor', 'CentralAir_N': 'No', 'CentralAir_Y': 'Yes', 'Electrical_SBrkr': 'Standard Circuit Breakers & Romex', 'Electrical_FuseA': 'Fuse Box over 60 AMP and all Romex wiring (Average)', 'Electrical_FuseF': '60 AMP Fuse Box and mostly Romex wiring (Fair)', 'Electrical_FuseP': '60 AMP Fuse Box and mostly knob & tube wiring (poor)', 'Electrical_Mix': 'Mixed', 'KitchenQual_Ex': 'Excellent', 'KitchenQual_Gd': 'Good', 'KitchenQual_TA': 'Typical/Average', 'KitchenQual_Fa': 'Fair', 'KitchenQual_Po': 'Poor', 'Functional_Typ': 'Typical Functionality', 'Functional_Min1': 'Minor Deductions 1', 'Functional_Min2': 'Minor Deductions 2', 'Functional_Mod': 'Moderate Deductions', 'Functional_Maj1': 'Major Deductions 1', 'Functional_Maj2': 'Major Deductions 2', 'Functional_Sev': 'Severely Damaged', 'Functional_Sal': 'Salvage only', 'FireplaceQu_Ex': 'Excellent - Exceptional Masonry Fireplace', 'FireplaceQu_Gd': 'Good - Masonry Fireplace in main level', 'FireplaceQu_TA': 'Average - Prefabricated Fireplace in main living area or Masonry Fireplace in basement', 'FireplaceQu_Fa': 'Fair - Prefabricated Fireplace in basement', 'FireplaceQu_Po': 'Poor - Ben Franklin Stove', 'FireplaceQu_NA': 'No Fireplace', 'GarageType_2Types': 'More than one type of garage', 'GarageType_Attchd': 'Attached to home', 'GarageType_Basment': 'Basement Garage', 'GarageType_BuiltIn': 'Built-In (Garage part of house - typically has room above garage)', 'GarageType_CarPort': 'Car Port', 'GarageType_Detchd': 'Detached from home', 'GarageType_NA': 'No Garage', 'GarageFinish_Fin': 'Finished', 'GarageFinish_RFn': 'Rough Finished', 'GarageFinish_Unf': 'Unfinished', 'GarageFinish_NA': 'No Garage', 'GarageQual_Ex': 'Excellent', 'GarageQual_Gd': 'Good', 'GarageQual_TA': 'Typical/Average', 'GarageQual_Fa': 'Fair', 'GarageQual_Po': 'Poor', 'GarageQual_NA': 'No Garage', 'GarageCond_Ex': 'Excellent', 'GarageCond_Gd': 'Good', 'GarageCond_TA': 'Typical/Average', 'GarageCond_Fa': 'Fair', 'GarageCond_Po': 'Poor', 'GarageCond_NA': 'No Garage', 'PavedDrive_Y': 'Paved', 'PavedDrive_P': 'Partial Pavement', 'PavedDrive_N': 'Dirt/Gravel', 'PoolQC_Ex': 'Excellent', 'PoolQC_Gd': 'Good', 'PoolQC_TA': 'Average/Typical', 'PoolQC_Fa': 'Fair', 'PoolQC_NA': 'No Pool', 'Fence_GdPrv': 'Good Privacy', 'Fence_MnPrv': 'Minimum Privacy', 'Fence_GdWo': 'Good Wood', 'Fence_MnWw': 'Minimum Wood/Wire', 'Fence_NA': 'No Fence', 'MiscFeature_Elev': 'Elevator', 'MiscFeature_Gar2': '2nd Garage (if not described in garage section)', 'MiscFeature_Othr': 'Other', 'MiscFeature_Shed': 'Shed (over 100 SF)', 'MiscFeature_TenC': 'Tennis Court', 'MiscFeature_NA': 'None', 'SaleType_WD': 'Warranty Deed - Conventional', 'SaleType_CWD': 'Warranty Deed - Cash', 'SaleType_VWD': 'Warranty Deed - VA Loan', 'SaleType_New': 'Home just constructed and sold', 'SaleType_COD': 'Court Officer Deed/Estate', 'SaleType_Con': 'Contract 15% Down payment regular terms', 'SaleType_ConLw': 'Contract Low Down payment and low interest', 'SaleType_ConLI': 'Contract Low Interest', 'SaleType_ConLD': 'Contract Low Down', 'SaleType_Oth': 'Other', 'SaleCondition_Normal': 'Normal Sale', 'SaleCondition_Abnorml': 'Abnormal Sale - trade, foreclosure, short sale', 'SaleCondition_AdjLand': 'Adjoining Land Purchase', 'SaleCondition_Alloca': 'Allocation - two linked properties with separate deeds, typically condo with a garage unit', 'SaleCondition_Family': 'Sale between family members', 'SaleCondition_Partial': 'Home was not completed when last assessed (associated with New Homes)'}
มาท่อนนี้เราจะทำการตัด Dictionary ในตัว Main_key ด้วยการ Split โดยตัวด้านหน้าหมายถึงชื่อ Columns จากนั้นค่อยเปลี่ยนตัวแปรด้วย การใช้ df[col].map แต่ทั้งนี้ใน map เราต้องเขียน lambda function เพื่อเอามาแค่ตัวที่เราจะใช้เท่านั้น
โดย Pattern คือ เราจะมองหาว่า main_key ที่เราตัดมา และตัว x ใน lambda function พอใส่เข้าไปแล้ว มันไปตรงกับ key ใน flattened_dict หรือไม่ ถ้าใช่ก็จะทำการ Map key ให้ตรงต่อไป แต่ถ้าไม่ใช่ก็จะ Return ค่าเดิมออกมา
หมายเหตุ : การใช้ map ใน Pandas จะทำให้ ตัวแปร main_key ถูกส่งผ่านไปยัง lambda function และ main_key จะกลายเป็นตัวแปร x ไปในทันที
for key in flattened_dict.keys():
main_key = key.split('_')[0] # แกะ Main Key ออกมา เช่นเอา "MSSubClass" ออกมาจาก "MSSubClass_20"
if main_key in df.columns: # เช็คว่า Main Key ตรงนี้เป็น Column ของ Dataframe หรือไม่ ถ้าใช่เราจะเปลี่ยนตัวแปรด้วยการทำ Map
df[main_key] = df[main_key].map(lambda x: flattened_dict.get(f"{main_key}_{x}", x))
df.head()
| Id | MSSubClass | MSZoning | LotFrontage | LotArea | Street | Alley | LotShape | LandContour | Utilities | ... | PoolArea | PoolQC | Fence | MiscFeature | MiscVal | MoSold | YrSold | SaleType | SaleCondition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 2-STORY 1946 & NEWER | Residential Low Density | 65.0 | 8450 | Paved | No alley access | Regular | Near Flat/Level | All public Utilities (E,G,W,& S) | ... | 0 | No Pool | No Fence | None | 0 | 2 | 2008 | Warranty Deed - Conventional | Normal Sale | 208500 |
| 1 | 2 | 1-STORY 1946 & NEWER ALL STYLES | Residential Low Density | 80.0 | 9600 | Paved | No alley access | Regular | Near Flat/Level | All public Utilities (E,G,W,& S) | ... | 0 | No Pool | No Fence | None | 0 | 5 | 2007 | Warranty Deed - Conventional | Normal Sale | 181500 |
| 2 | 3 | 2-STORY 1946 & NEWER | Residential Low Density | 68.0 | 11250 | Paved | No alley access | Slightly irregular | Near Flat/Level | All public Utilities (E,G,W,& S) | ... | 0 | No Pool | No Fence | None | 0 | 9 | 2008 | Warranty Deed - Conventional | Normal Sale | 223500 |
| 3 | 4 | 2-STORY 1945 & OLDER | Residential Low Density | 60.0 | 9550 | Paved | No alley access | Slightly irregular | Near Flat/Level | All public Utilities (E,G,W,& S) | ... | 0 | No Pool | No Fence | None | 0 | 2 | 2006 | Warranty Deed - Conventional | Abnormal Sale - trade, foreclosure, short sale | 140000 |
| 4 | 5 | 2-STORY 1946 & NEWER | Residential Low Density | 84.0 | 14260 | Paved | No alley access | Slightly irregular | Near Flat/Level | All public Utilities (E,G,W,& S) | ... | 0 | No Pool | No Fence | None | 0 | 12 | 2008 | Warranty Deed - Conventional | Normal Sale | 250000 |
5 rows × 81 columns
โอเค ! ดูเหมือนผลลัพธ์ใช้ได้ ทีนี้ลองมา print สแกนๆ ดูดีกว่าว่าเป็นอย่างไร
df_object_cols = df.select_dtypes(include=['object']).columns
for col in df_object_cols:
print(f"Column '{col}' unique values: ", df[col].unique())
Column 'MSSubClass' unique values: ['2-STORY 1946 & NEWER' '1-STORY 1946 & NEWER ALL STYLES' '2-STORY 1945 & OLDER' '1-1/2 STORY FINISHED ALL AGES' '2 FAMILY CONVERSION - ALL STYLES AND AGES' '1-1/2 STORY - UNFINISHED ALL AGES' 'DUPLEX - ALL STYLES AND AGES' '1-STORY PUD (Planned Unit Development) - 1946 & NEWER' '1-STORY 1945 & OLDER' 'SPLIT FOYER' 'SPLIT OR MULTI-LEVEL' '2-STORY PUD - 1946 & NEWER' '2-1/2 STORY ALL AGES' 'PUD - MULTILEVEL - INCL SPLIT LEV/FOYER' '1-STORY W/FINISHED ATTIC ALL AGES'] Column 'MSZoning' unique values: ['Residential Low Density' 'Residential Medium Density' 'C (all)' 'Floating Village Residential' 'Residential High Density'] Column 'Street' unique values: ['Paved' 'Gravel'] Column 'Alley' unique values: ['No alley access' 'Gravel' 'Paved'] Column 'LotShape' unique values: ['Regular' 'Slightly irregular' 'Moderately Irregular' 'Irregular'] Column 'LandContour' unique values: ['Near Flat/Level' 'Banked - Quick and significant rise from street grade to building' 'Depression' 'Hillside - Significant slope from side to side'] Column 'Utilities' unique values: ['All public Utilities (E,G,W,& S)' 'Electricity and Gas Only'] Column 'LotConfig' unique values: ['Inside lot' 'Frontage on 2 sides of property' 'Corner lot' 'Cul-de-sac' 'Frontage on 3 sides of property'] Column 'LandSlope' unique values: ['Gentle slope' 'Moderate Slope' 'Severe Slope'] Column 'Neighborhood' unique values: ['College Creek' 'Veenker' 'Crawford' 'Northridge' 'Mitchell' 'Somerset' 'Northwest Ames' 'Old Town' 'Brookside' 'Sawyer' 'Northridge Heights' 'NAmes' 'Sawyer West' 'Iowa DOT and Rail Road' 'Meadow Village' 'Edwards' 'Timberland' 'Gilbert' 'Stone Brook' 'Clear Creek' 'Northpark Villa' 'Bloomington Heights' 'Briardale' 'South & West of Iowa State University' 'Bluestem'] Column 'Condition1' unique values: ['Normal' 'Adjacent to feeder street' 'Near positive off-site feature--park, greenbelt, etc.' 'Adjacent to arterial street' 'Adjacent to East-West Railroad' "Within 200' of North-South Railroad" 'Adjacent to North-South Railroad' 'Adjacent to postive off-site feature' "Within 200' of East-West Railroad"] Column 'Condition2' unique values: ['Normal' 'Adjacent to arterial street' "Within 200' of North-South Railroad" 'Adjacent to feeder street' 'Near positive off-site feature--park, greenbelt, etc.' 'Adjacent to postive off-site feature' 'Adjacent to North-South Railroad' 'Adjacent to East-West Railroad'] Column 'BldgType' unique values: ['Single-family Detached' '2fmCon' 'Duplex' 'Townhouse End Unit' 'Twnhs'] Column 'HouseStyle' unique values: ['2Story' 'One story' '1.5Fin' '1.5Unf' 'SFoyer' 'SLvl' '2.5Unf' '2.5Fin'] Column 'OverallQual' unique values: ['Good' 'Above Average' 'Very Good' 'Average' 'Excellent' 'Below Average' 'Very Excellent' 'Fair' 'Very Poor' 'Poor'] Column 'OverallCond' unique values: ['Average' 'Very Good' 'Above Average' 'Good' 'Below Average' 'Poor' 'Fair' 'Excellent' 'Very Poor'] Column 'RoofStyle' unique values: ['Gable' 'Hip' 'Gabrel (Barn)' 'Mansard' 'Flat' 'Shed'] Column 'RoofMatl' unique values: ['Standard (Composite) Shingle' 'Wood Shingles' 'Metal' 'Wood Shakes' 'Membrane' 'Gravel & Tar' 'Roll' 'Clay or Tile'] Column 'Exterior1st' unique values: ['Vinyl Siding' 'Metal Siding' 'Wood Siding' 'Hard Board' 'Brick Face' 'Wood Shingles' 'Cement Board' 'Plywood' 'Asbestos Shingles' 'Stucco' 'Brick Common' 'Asphalt Shingles' 'Stone' 'Imitation Stucco' 'Cinder Block'] Column 'Exterior2nd' unique values: ['Vinyl Siding' 'Metal Siding' 'Wd Shng' 'Hard Board' 'Plywood' 'Wood Siding' 'CmentBd' 'Brick Face' 'Stucco' 'Asbestos Shingles' 'Brk Cmn' 'Imitation Stucco' 'Asphalt Shingles' 'Stone' 'Other' 'Cinder Block'] Column 'MasVnrType' unique values: ['Brick Face' 'None' 'Stone' 'Brick Common'] Column 'ExterQual' unique values: ['Good' 'Average/Typical' 'Excellent' 'Fair'] Column 'ExterCond' unique values: ['Average/Typical' 'Good' 'Fair' 'Poor' 'Excellent'] Column 'Foundation' unique values: ['Poured Contrete' 'Cinder Block' 'Brick & Tile' 'Wood' 'Slab' 'Stone'] Column 'BsmtQual' unique values: ['Good (90-99 inches)' 'Typical (80-89 inches)' 'Excellent (100+ inches)' 'No Basement' 'Fair (70-79 inches)'] Column 'BsmtCond' unique values: ['Typical - slight dampness allowed' 'Good' 'No Basement' 'Fair - dampness or some cracking or settling' 'Poor - Severe cracking, settling, or wetness'] Column 'BsmtExposure' unique values: ['No Exposure' 'Good Exposure' 'Mimimum Exposure' 'Average Exposure (split levels or foyers typically score average or above)' 'No Basement'] Column 'BsmtFinType1' unique values: ['Good Living Quarters' 'Average Living Quarters' 'Unfinshed' 'Average Rec Room' 'Below Average Living Quarters' 'No Basement' 'Low Quality'] Column 'BsmtFinType2' unique values: ['Unfinshed' 'Below Average Living Quarters' 'No Basement' 'Average Living Quarters' 'Average Rec Room' 'Low Quality' 'Good Living Quarters'] Column 'Heating' unique values: ['Gas forced warm air furnace' 'Gas hot water or steam heat' 'Gravity furnace' 'Wall furnace' 'Hot water or steam heat other than gas' 'Floor Furnace'] Column 'HeatingQC' unique values: ['Excellent' 'Good' 'Average/Typical' 'Fair' 'Poor'] Column 'CentralAir' unique values: ['Yes' 'No'] Column 'Electrical' unique values: ['Standard Circuit Breakers & Romex' '60 AMP Fuse Box and mostly Romex wiring (Fair)' 'Fuse Box over 60 AMP and all Romex wiring (Average)' '60 AMP Fuse Box and mostly knob & tube wiring (poor)' 'Mixed' 'NA'] Column 'KitchenQual' unique values: ['Good' 'Typical/Average' 'Excellent' 'Fair'] Column 'Functional' unique values: ['Typical Functionality' 'Minor Deductions 1' 'Major Deductions 1' 'Minor Deductions 2' 'Moderate Deductions' 'Major Deductions 2' 'Severely Damaged'] Column 'FireplaceQu' unique values: ['No Fireplace' 'Average - Prefabricated Fireplace in main living area or Masonry Fireplace in basement' 'Good - Masonry Fireplace in main level' 'Fair - Prefabricated Fireplace in basement' 'Excellent - Exceptional Masonry Fireplace' 'Poor - Ben Franklin Stove'] Column 'GarageType' unique values: ['Attached to home' 'Detached from home' 'Built-In (Garage part of house - typically has room above garage)' 'Car Port' 'No Garage' 'Basement Garage' 'More than one type of garage'] Column 'GarageFinish' unique values: ['Rough Finished' 'Unfinished' 'Finished' 'No Garage'] Column 'GarageQual' unique values: ['Typical/Average' 'Fair' 'Good' 'No Garage' 'Excellent' 'Poor'] Column 'GarageCond' unique values: ['Typical/Average' 'Fair' 'No Garage' 'Good' 'Poor' 'Excellent'] Column 'PavedDrive' unique values: ['Paved' 'Dirt/Gravel' 'Partial Pavement'] Column 'PoolQC' unique values: ['No Pool' 'Excellent' 'Fair' 'Good'] Column 'Fence' unique values: ['No Fence' 'Minimum Privacy' 'Good Wood' 'Good Privacy' 'Minimum Wood/Wire'] Column 'MiscFeature' unique values: ['None' 'Shed (over 100 SF)' '2nd Garage (if not described in garage section)' 'Other' 'Tennis Court'] Column 'SaleType' unique values: ['Warranty Deed - Conventional' 'Home just constructed and sold' 'Court Officer Deed/Estate' 'Contract Low Down' 'Contract Low Interest' 'Warranty Deed - Cash' 'Contract Low Down payment and low interest' 'Contract 15% Down payment regular terms' 'Other'] Column 'SaleCondition' unique values: ['Normal Sale' 'Abnormal Sale - trade, foreclosure, short sale' 'Home was not completed when last assessed (associated with New Homes)' 'Adjoining Land Purchase' 'Allocation - two linked properties with separate deeds, typically condo with a garage unit' 'Sale between family members']
ดูจาก Output แล้วไม่น่ามีปัญหาอะไร เข้าสู่ Part ถัดไปกันดีกว่า
ใน Part นี้เราจะพูดถึงการทำความเข้าใจข้อมูลในด้านต่างๆ โดยหลักๆ จะเน้นไปที่
ส่วนใน Part ของฝั่ง
ไว้มีโอกาสจะมาทำให้นะครับ 😆
กลับเข้าเรื่องของการ Visualized สิ่งแรกที่ต้องทำคงจะหนีไม่พ้นการ Import Library ที่ทำงานด้านนี้ได้อย่างดีเยี่ยมทั้งสองตัวอย่าง Seaborn และ Matplotlib
import seaborn as sns
import matplotlib.pyplot as plt
#import ไปก่อนเพื่อได้ใช้
import warnings
warnings.filterwarnings("ignore")
plt.figure(figsize=(12,8))
sns.heatmap(df.corr("pearson"),cmap="RdBu")
plt.title("Correlations Between Variables", size=15)
plt.show()
เราลอง Plot SalePrice ใน 3 รูปแบบดังนี้
แล้วดูว่าการกระจายตัวของข้อมูลของเรามันทับกับแบบไหน
Note : ถึงแม้คราวนี้จะไม่ได้ทำเกี่ยวกับ Machine Learning แต่โดยปกติแล้ว ในการทำ Machine Learning Model ส่วนใหญ่มักจะชอบข้อมูลที่มีการกระจายตัวแบบ Normal Distribution ซึ่งถ้าข้อมูลดิบที่ให้มามันไม่ได้กระจายตัวแบบนั้น ก็ต้องแปลงร่างมันอีกที
import scipy.stats as st
y = df['SalePrice']
plt.figure(1); plt.title('Johnson SU')
sns.distplot(y, kde=True, fit=st.johnsonsu)
plt.figure(2); plt.title('Normal')
sns.distplot(y, kde=True, fit=st.norm)
plt.figure(3); plt.title('Log Normal')
sns.distplot(y, kde=True, fit=st.lognorm)
<Axes: title={'center': 'Log Normal'}, xlabel='SalePrice', ylabel='Density'>
จะเห็นว่าการกระจายของเราเป็น Log Normal ไม่ก็ Johnson SU
สำหรับการตรวจสอบ Sensitivity ปัญหาของข้อมูลประเภท Categorial คือมันไม่สามารถจัดลำดับมันได้โดยตรง
ทำให้เราต้องอาศัยเทคนิคทางสถิติที่เรียกว่า ANOVA ที่มีหลักการคือการหาค่าเฉลี่ยของแต่ละกลุ่ม และค่าเฉลี่ยของทุกคน แล้วมาเทียบกันว่า พอตัวแปรเปลี่ยนกลุ่มแล้ว ค่าที่ได้มันเบี่ยงเบนออกจากค่าเฉลี่ยเดิมมากน้อยขนาดไหน ถ้าเบี่ยงมากแปลว่าตัวแปรนี้มีผลต่อค่าเฉลี่ยสูง เป็นต้น
โดยเราต้องใช้ Library อย่าง numpy และ scipy เข้ามาช่วย (ไม่รู้ Import ด้านบนไปหรือยัง)
import scipy.stats as stats
import numpy as np
categorical_columns = df.select_dtypes(include=['object']).columns
anova_results = []
for column in categorical_columns:
groups = [df['SalePrice'][df[column] == category] for category in df[column].unique()]
f_val, p_val = stats.f_oneway(*groups)
anova_results.append({'Variable': column, 'F-value': f_val, 'p-value': p_val})
anova_results_df = pd.DataFrame(anova_results)
plt.figure(figsize=(10, 6))
sns.barplot(data=anova_results_df, y='Variable', x=-np.log10(anova_results_df['p-value']))
plt.xlabel('-Log10(p-value)')
plt.title('ANOVA results')
plt.show()
ปกติแล้วการทำ ANOVA ทั้ง f-value และ p-value ต่างเอามาใช้บอกความแรง(Sensitivity) ได้ทั้งคู่ ดังนั้นเพื่อคมุมมองที่มากกว่า เราจะใช้ทั้งคู่ไปเลย
โดยที่
ทั้งนี้สำหรับการเรียงลำดับ เราจึงนำเอา p-value มาปรับเป็น -log scale อีกทีเพื่อให้ค่า p-value ที่น้อยที่สุดแสดงผลได้มากที่สุด
anova_results_df = anova_results_df.sort_values(by='p-value', ascending=True)
plt.figure(figsize=(10,6))
sns.barplot(data=anova_results_df, x='Variable', y=-np.log10(anova_results_df['p-value']))
plt.xlabel('-Log10(p-value)')
plt.title('ANOVA p-values for Categorical Variables')
x=plt.xticks(rotation=90)
plt.show()
anova_results_df = anova_results_df.sort_values(by='F-value', ascending=False)
plt.figure(figsize=(10,6))
sns.barplot(data=anova_results_df, x='Variable', y='F-value')
plt.xlabel('F-value')
plt.title('ANOVA F-values for Categorical Variables')
x=plt.xticks(rotation=90)
plt.show()
หลังจากทำ Anova จะเห็นว่าตัวแปร ExterQual,OverallQual,BsmentQual,KitchenQual,Neighberhood
ดูจะเป็นตัวแปรที่ส่งผลกระทบกับ SalePrice อย่างมีนัยยะสำคัญ ในขณะที่ตัวแปรอย่างเช่นสาธารณูปโภค การติด Heating หรือ Aircondition ดูไม่ค่อยสำคัญเท่าไร
ทั้งนี้ถ้าให้เดาคิดว่าตัวแปรที่ไม่ค่อยสำคัญ น่าจะเป็นอะไรที่มัน Common สำหรับทุกบ้าน หรืออาจจะไม่ต่างกันมากในแต่ละกลุ่ม
อีกวิธีที่ดูความสัมพันธ์ระหว่างตัวแปรคือการหา Correlation ทั้งนี้เราเลือกใช้ Spearman Correlation ในการตรวจสอบว่าตัวแปรไหนส่งผลต่อ SalePrice มากที่สุด เนื่องจาก Spearman Correlation เหมาะสำหรับตรวจสอบตัวแปรสองตัวที่มีแนวโน้มเคลื่อนที่ตามกัน และจุดที่สำคัญคือตัวแปรสองตัวไม่จำเป็นต้องมีความสัมพันธ์แบบเส้นตรงแบบ Pearson Correlation
แต่อย่าลืมว่าตัวแปรบางอย่างไม่ได้เป็นตัวแปรที่เป็นตัวเลข ดังนั้นวิธีการที่เราต้องใช้จัดการกับข้อมูลเชิงคุณภาพคือ การหาค่าเฉลี่ยของราคาแต่ละกลุ่ม จากนั้นเอาค่าเฉลี่ยของราคาบ้านในแต่ละกลุ่มตัวแปรมาทำ Correlation เพื่อหาความสัมพันธ์อีกที
df_copy = df.copy()
categorical_columns = df_copy.select_dtypes(include=['object']).columns
# เปลี่ยน Categorial Data แต่ละตัวด้วยค่าเฉลี่ยแทน
for column in categorical_columns:
mean_saleprice = df_copy.groupby(column)['SalePrice'].mean()
df_copy[column] = df_copy[column].map(mean_saleprice)
# แปลงข้อมูลให้เป็น Numeric แล้วทำ Spearman
spearman_corr_df = df_copy.corr(method='spearman')['SalePrice'].sort_values(ascending=False).reset_index()
spearman_corr_df.columns = ['Feature', 'Spearman_Correlation']
spearman_corr_df = spearman_corr_df[spearman_corr_df['Feature'] != 'SalePrice']
plt.figure(figsize=(10, len(spearman_corr_df) / 5))
sns.barplot(data=spearman_corr_df, y='Feature', x='Spearman_Correlation', orient='h')
plt.title('Spearman Correlation of Features with SalePrice')
plt.show()
และด้วยความที่ตัวแปรมันเยอะมาก และค่า Correlation มีทั้งบวกและลบ ผมเลยตัดมาแค่ตัวแปรที่สำคัญที่สุด 15 อันดับแรกที่มีค่า Correlation สูงสุด เพื่อมาหาว่าตัวแปรไหนส่งผลต่อราคาขายบ้านมากที่สุด
spearman_corr_df = spearman_corr_df.head(15)
plt.figure(figsize=(10, len(spearman_corr_df) / 2))
sns.barplot(data=spearman_corr_df, y='Feature', x='Spearman_Correlation', orient='h')
plt.title('Top 15 Features by Spearman Correlation with SalePrice')
plt.show()
จะเห็นว่าตัวแปรในกลุ่มที่เป็นคุณภาพ ก็ได้ผลลัพธ์ค่าเดิมๆ คล้ายๆกับที่ทำ ANOVA ในขณะที่ตัวแปรที่เป็นตัวเลขก็จะเกี่ยวข้องกับพื้นที่ของตัวบ้าน เช่น
อีกตัวแปรที่น่าสนใจไม่แพ้กันคือปีที้สร้างหรือ YearBuilt
ผมสมมุติว่าได้ข้อมูลมาก้อนหนึ่ง โดยที่ไม่ได้รู้ Context อะไรเลยเกี่ยวกับข้อมูล
หลังจากที่ทำ ANOVA แล้ว ยังมีคำถามที่ผมสงสัย(อันนี้สงสัยเอง) เลยจะลองไล่หาคำตอบดูครับ
คำถามที่สงสัยได้แก่
บรรทัดนี้พอดีจะ Map Dictionary ใหม่ เพราะพึ่งเห็นว่า OverallQual มันเป็นคะแนน ควรจะเรียงถึง 1-10
nested_dict = enum_dict_2['OverallQual']
OverallQualMapping = nested_dict['sub_keys']
inverted_mapping = {v: k for k, v in OverallQualMapping.items()}
df['OverallQualScore'] = df['OverallQual'].map(inverted_mapping).astype(int)
import seaborn as sns
sorted_labels = sorted(inverted_mapping, key=lambda k: int(inverted_mapping[k]))
plt.figure(figsize=(12, 6))
sns.boxplot(x='OverallQual', y='SalePrice', data=df,order=sorted_labels)
plt.title('Sale Price vs Overall Quality')
plt.xlabel('Overall Quality')
plt.ylabel('Sale Price')
plt.show()
quality_columns = []
non_quality_columns = []
for column in categorical_columns:
if column.endswith('Qual') or column.endswith('QC') or column.endswith('Qu'):
quality_columns.append(column)
else:
non_quality_columns.append(column)
ปกติแล้วสำหรับ Categorial Data ผมจะชอบ Plot ข้อมูลออกมาด้วย Box Plot แล้วลองดูว่ามีข้อมูลชุดไหนบ้างที่น่าสนใจค้นคว้าต่อ
Note : ลอง Plot แบบปกติแล้วพบว่าดูไม่รู้เรื่องเลย เลยต้องเปลี่ยนเป็นการทำ BoxPlot ในแกน Y แทน
def draw_horizontal_boxplots(data, y_col):
plt.figure(figsize=(10, len(data.columns) * 4))
for i, col in enumerate(data.columns):
if col != y_col:
plt.subplot(len(data.columns)-1, 1, i+1)
ax = sns.boxplot(x=y_col, y=col, data=data, orient='h')
ax.set_ylabel(col, rotation=0, va='bottom', ha='right')
plt.tight_layout()
feature_only = df[non_quality_columns]
feature_only['SalePrice'] = df['SalePrice']
draw_horizontal_boxplots(feature_only, 'SalePrice')
qual_only = df[quality_columns]
qual_only['SalePrice']=df['SalePrice']
draw_horizontal_boxplots(qual_only, 'SalePrice')
ลองทำ Scatter Plot จะเห็นว่าบ้านส่วนใหญ่คุณภาพดี ราคาอยู่ในโซน 2-3 แสนเหรียญ และพื้นที่ก็อยู่ในช่วง 1000-2000 sqr-feet
Note : 1000 sqr-feet = 92.9 ตารางเมตร
plt.figure(figsize=(12, 8))
sns.scatterplot(data=df, x='GrLivArea', y='SalePrice', hue='OverallQual', palette='viridis', sizes=(20, 200), alpha=0.7)
plt.title('Scatterplot of SalePrice vs GrLivArea colored by OverallQual')
plt.show()
อีกตัวแปรที่ Sensitivity ไวพอๆ กับ GrLiving คือคุณภาพครัว ผมคิดว่าคนสมัยใหม่ชอบทำกับข้าว ดังนั้นปัจจัยนี้น่าจะเป็นตัวเลือกที่สำคัญไม่แพ้กัน
ด้วยความอยากรู้ผมเลย Plot Percentage ว่าสัดส่วนบ้านที่สร้างในแต่ละยุค คุณภาพของครัวเป็นอย่างไร จะเห็นว่าในช่วงยุค 1970s เราจะเริ่มเห็นครัวแบบ Good และพอมายุค 2000s เราเริ่มเห็นครัวแบบ Excellent เป็นสัดส่วนที่มากขึ้น
df['DecadeBuilt'] = (10 * (df['YearBuilt'] // 10)).astype(str) + 's'
DecadeBuilt_KitchenQual_grouped = df.groupby(['DecadeBuilt', 'KitchenQual']).size().reset_index(name='Count')
pivot_data = DecadeBuilt_KitchenQual_grouped.pivot(index='DecadeBuilt', columns='KitchenQual', values='Count').fillna(0)
pivot_percentage = pivot_data.divide(pivot_data.sum(axis=1), axis=0) * 100
plt.figure(figsize=(14, 8))
for column in pivot_percentage.columns:
sns.lineplot(data=pivot_percentage, x=pivot_percentage.index, y=column, label=column, marker='o')
plt.title('Percentage of KitchenQual by DecadeBuilt')
plt.ylabel('Percentage (%)')
plt.xlabel('Decade Built')
plt.legend(title='KitchenQual')
plt.show()
ตอนแรกนึกว่าบ้านเก่าๆ อาจจะแพงเพราะ Classic ก็ได้ แต่หลักฐานก็คือราคาบ้านสัมพันธ์กับปีที่สร้างครับ
fig, ax = plt.subplots(figsize=(8, 6))
sns.regplot(x='YearBuilt', y='SalePrice', data=df, ax=ax, line_kws={"color": "red"})
ax.set_title('Year Built vs Sale Price')
ax.set_xlabel('Year Built')
ax.set_ylabel('Sale Price')
plt.show()
neighborhood_counts = df['Neighborhood'].value_counts()
neighborhood_counts.plot(kind='bar', figsize=(10,6))
plt.title('Number of Sales by Neighborhood')
plt.xlabel('Neighborhood')
plt.ylabel('Number of Sales')
plt.show()
ตอนแรกแอบเดาในใจว่าบ้านในย่านนี้ถูกสร้างช่วงปียุคหลังๆ นี่แหละ แต่พอลองเข้าไป Plot ดูจึงพบว่าไม่ใช่ เทรนด์การสร้างบ้านในย่านนี้ลดลงอย่างเห็นได้ชัด
df['DecadeBuilt'] = (10 * (df['YearBuilt'] // 10)).astype(str) + 's'
nam_filter = df[df['Neighborhood'] == 'NAmes'].copy()
decade_counts = nam_filter.groupby('DecadeBuilt').size()
decade_counts.plot(kind='line', marker='o', figsize=(10,6))
plt.title("Trend of Houses Built in 'NAmes' Neighborhood Over Decades")
plt.ylabel("Number of Houses")
plt.xlabel("Decade Built")
plt.grid(True)
plt.show()
grouped_df = df.groupby(['Neighborhood', 'DecadeBuilt']).size().reset_index(name='HouseCount')
# ใช้ FacetGrid ในการ Plot หลายภาพพร้อมกัน
g = sns.FacetGrid(grouped_df, col="Neighborhood", col_wrap=5, height=4, margin_titles=True, sharey=False)
g.map_dataframe(sns.lineplot, x="DecadeBuilt", y="HouseCount", marker="o")
g.set_titles(col_template="{col_name}")
g.set_axis_labels("Decade Built", "Number of Houses Built")
g.set_xticklabels(rotation=45)
g.tight_layout()
plt.show()
ลองทำ Scatter Plot สวยๆ ดูครับ นึกว่าจะดี ดูไม่รู้เรื่องซะงั้น
plt.figure(figsize=(15, 10))
sns.scatterplot(data=df, x="YearBuilt", y="SalePrice", hue="Neighborhood", palette="tab20", edgecolor=None, alpha=0.7)
plt.title("Sale Price by Year Built and Neighborhood")
plt.xlabel("Year Built")
plt.ylabel("Sale Price")
plt.xticks(rotation=45)
plt.legend(title="Neighborhood", bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
neighborhood_data = df.groupby('Neighborhood')['SalePrice'].agg(['count', 'mean'])
neighborhood_data.sort_values('count', ascending=False, inplace=True)
neighborhood_data.reset_index(inplace=True)
fig, ax1 = plt.subplots(figsize=(10,6))
sns.barplot(x='Neighborhood', y='count', data=neighborhood_data, ax=ax1, palette="Blues_d")
ax1.set_ylabel('Number of Sales')
ax2 = ax1.twinx()
sns.lineplot(x='Neighborhood', y='mean', data=neighborhood_data, ax=ax2, color='r', sort=False)
ax2.set_ylabel('Average Sale Price')
ax1.set_xticklabels(ax1.get_xticklabels(), rotation=90)
plt.title('Number of Sales and Average Sale Price by Neighborhood')
plt.tight_layout()
plt.show()
ตอนแรกที่ขะตอบคำถามนี้ ผมทำ Scatter Plot ออกมาเพื่อจะดูจำนวนจุดว่ามีการสร้างโรงจอดเยอะจริงไหม แต่ลืมนึกไปว่าพอเศรษฐกิจโตขึ้น การสร้างบ้านควรจะเยอะขึ้นไปด้วย
ผมคิดว่าการทำ Scatter Plot ด้านบนอาจจะบอกไม่หมด เราอาจจะเห็นว่าพื้นที่โรงรถเยอะขึ้นก็จริง แต่อาจจะลืมนึกไปว่าบ้านที่มีพื้นที่โรงรถอาจจะเป็นส่วนน้อยเทียบกับบ้านที่สร้างในปีเดียวกันหรือไม่
มากไปกว่านั้น ไม่แน่ว่าระบบขนส่งสารธารณะอาจจะดีจนบ้านไม่จำเป็นต้องมีที่จอดก็ได้ ผมเลยลองหาสัดส่วนบ้านที่ไม่มีที่จอดลดในแต่ละ Decade ว่ามีแนวโน้มเพิ่มขึ้นหรือลดลงกันแน่
plt.figure(figsize=(12, 7))
sns.regplot(x=df['YearBuilt'], y=df['GarageArea'], scatter_kws={'s': 20, 'alpha': 0.5}, line_kws={'color': 'red'})
plt.title('Trend of GarageArea by YearBuilt')
plt.xlabel('Year Built')
plt.ylabel('Garage Area')
plt.tight_layout()
plt.show()
df['HasGarage'] = df['GarageArea'] > 0
df['DecadeBuilt'] = (10 * (df['YearBuilt'] // 10)).astype(str) + 's'
garage_decade_group = df.groupby('DecadeBuilt')['HasGarage'].apply(lambda x: 100 * (1 - x.mean()))
plt.figure(figsize=(12, 7))
garage_decade_group.sort_index().plot(kind='line', marker='o', color='skyblue')
plt.title('Percentage of Houses Without a Garage by Decade')
plt.xlabel('Decade Built')
plt.ylabel('Percentage Without Garage (%)')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.tight_layout()
plt.show()
เนื่องจากทำเลเป็นอะไรที่มีผลต่อราคาบ้านอย่างยิ่ง เราจะมาทำการศึกษากันต่อว่ากลุ่มผู้ซื้อบ้านแต่ละกลุ่ม อาศัยอยู่ที่ทำเลไหนกันบ้าง
โดยเราจะทำการแบ่งกลุ่มผู้ซื้อบ้านจากราคาขายเป็น 4 กลุ่มได้แก่
Medium Price : กลุ่มราคาระหว่าง 25%-75%
High Price : กลุ่มราคาระหว่าง 75% ขึ้นไปจนถึง 95%
การแบ่งกลุ่มครั้งนี้เพื่อจะทำความเข้าใจในย่านต่างๆ ว่าคนมีฐานะอาศัยอยู่โซนไหน คนฐานะกลางๆ อาศัยอยู่บริเวณไหนเป็นต้น
โดยจะ Visualized ข้อมูลด้วย Nightingale Chart (เพราะลองทำ Grouped Bar Chart แล้วไม่ Work ครับ ดูไม่รู้เรื่องเลย)
import numpy as np
cut_bins = [0,
df['SalePrice'].quantile(0.25),
df['SalePrice'].quantile(0.75),
df['SalePrice'].quantile(0.95),
df['SalePrice'].max()]
cut_labels = ['Low Price', 'Medium Price', 'High Price', 'Luxury']
df['PriceCategory'] = pd.cut(df['SalePrice'], bins=cut_bins, labels=cut_labels, include_lowest=True)
ct = pd.crosstab(df['Neighborhood'], df['PriceCategory'])
#บิดกราฟให้เป็น r-thetha co-ordinate
theta = np.linspace(0.0, 2 * np.pi, len(ct), endpoint=False)
colors = ['red', 'green', 'blue', 'purple']
for column, color in zip(ct.columns, colors):
fig, ax = plt.subplots(figsize=(12, 12), subplot_kw={'projection': 'polar'})
values = ct[column].values
ax.bar(theta, values, alpha=0.6, label=column, color=color)
ax.set_title(f"Distribution of {column} by Neighborhood")
ax.set_xticks(theta)
ax.set_xticklabels(ct.index, size=10)
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1) # ให้มันหมุนตามเข็ม
plt.show()
จากข้อมูล เราพอจะเห็นภาพมากขึ้นแล้วว่าคนซื้อบ้านฐานะต่างๆ อาศัยอยู่บริเวณไหนกันบ้าง
กลุ่ม Low Price : เน้นอาศัยอยู่ย่าน Old Town,Edwars,Sawyer
Medium Price : เน้นอาศัยอยู่ย่าน Names,Mitchell,NorthPark Villa
High Price : เน้นอาศัยอยู่ย่าน College Creek, Crawford, Clear Creek
Luxury : เน้นอาศัยอยู่ย่าน Northridge,Northridge Heights, Stone Brook
เราลอง Plot Heatmap โดย Plot ย่านเรียงตามราคา เพื่อดูว่าบ้านที่แพงสุดไปถึงถูกสุดมีลักษณะการสร้างอย่างไร
จากข้อมูลก็พบว่าบ้านชั้นเดียว (One-Story) ดูจะเป็นอะไรที่ได้รับความนิยมมากไม่ว่าจะย่านไหน
แต่จุดที่น่าสังเกตุคือในย่านที่ราคาบ้านเฉลี่ยๆ ถูกๆ จะเริ่มเห็นบ้านแบบ 1.5Fin (บ้านที่มีชั้นสองเป็นสัดส่วนครึ่งหนึ่งของฐานชั้นแรก) ให้เห็นบ้าง
และที่สำคัญคือบ้านชั้นเดียว ไม่ได้แปลว่าถูกนะครับ ทำออกมาราคาเฉลี่ยๆ ก็ไม่ได้แย่เลย
house_styles_count = df['HouseStyle'].value_counts().sort_values(ascending=False)
avg_price_per_style = df.groupby('HouseStyle')['SalePrice'].mean()[house_styles_count.index]
plt.figure(figsize=(14, 6))
ax = house_styles_count.plot(kind='bar', color='skyblue', position=0, width=0.4, align='center')
ax2 = ax.twinx()
avg_price_per_style.plot(kind='line', marker='o', ax=ax2, color='red', linewidth=2, label='Average Sale Price')
ax.set_title('Number of Houses Sold & Average Sale Price by HouseStyle')
ax.set_xlabel('HouseStyle')
ax.set_ylabel('Number of Houses Sold')
ax2.set_ylabel('Average Sale Price ($)')
ax.set_xticklabels(house_styles_count.index, rotation=45)
ax.legend(loc='upper left')
ax2.legend(loc='upper right')
plt.tight_layout()
plt.show()
จากข้อมูลปีที่สร้างบ้านกับลักษณะบ้านจะเห็นว่าสมัยก่อนบ้านแบบ 1.5Story กับบ้านสองชั้นได้รับความนิยม แต่พอมายุคหลังๆ เทรนด์เปลี่ยน คนหันมาชอบบ้านชั้นเดียวมากขึ้น และแน่นอนว่า North Ames เป็นที่ที่นำเทรนด์สร้างบ้านชั้นเดียว สอดคล้องกับด้านบนที่บอกว่าบ้านย่าน North Amnes สร้างในยุค 50s-60s แบบรัวๆ
grouped_housestyle = df.groupby(['DecadeBuilt', 'HouseStyle']).size().reset_index(name='Count')
pivot_housestyle = grouped_housestyle.pivot(index='DecadeBuilt', columns='HouseStyle', values='Count').fillna(0)
housestyle_percentage = pivot_housestyle.divide(pivot_housestyle.sum(axis=1), axis=0) * 100
ax = housestyle_percentage.plot(kind='area', figsize=(12, 7), alpha=0.5)
plt.title('Percentage of Each HouseStyle Built Over Decades')
plt.ylabel('Percentage')
plt.xlabel('Decade Built')
plt.legend(title='HouseStyle')
plt.tight_layout()
plt.show()
average_prices = df.groupby('Neighborhood')['SalePrice'].mean().sort_values(ascending=False)
contingency_table = pd.crosstab(df['Neighborhood'], df['HouseStyle'], values=df['SalePrice'], aggfunc='count').reindex(average_prices.index).fillna(0)
plt.figure(figsize=(14, 10))
sns.heatmap(contingency_table, annot=True, cmap='viridis', fmt='g')
plt.title('Relationship between HouseStyle and Neighborhood (sorted by Average SalePrice)')
plt.xlabel('HouseStyle')
plt.ylabel('Neighborhood')
plt.tight_layout()
plt.show()
sales_counts = df.groupby(['Neighborhood', 'YrSold']).size().reset_index(name='Count')
sales_pivot = sales_counts.pivot('Neighborhood', 'YrSold', 'Count').fillna(0)
sorted_neighborhoods = sales_pivot.sum(axis=1).sort_values(ascending=False).index
sales_pivot = sales_pivot.reindex(sorted_neighborhoods)
plt.figure(figsize=(12, 10))
sns.heatmap(sales_pivot, annot=True, fmt=".0f", cmap='viridis', cbar_kws={'label': 'Number of Houses Sold'})
plt.title('Number of Houses Sold by Year and Neighborhood')
plt.xlabel('Year Sold')
plt.ylabel('Neighborhood')
plt.show()
ทีนี้ลองมาทำ Heatmap เพื่อดูความสัมพันธ์ระหว่าง Condition1 กับ Neighborhood โดยตั้งสมมุติฐานว่าบ้านคนรวยต้องขอเงื่อนไขอะไรแปลกๆ แน่
แต่ปรากฏว่าไม่ใช่แบบนั้น บ้านส่วนใหญ่ขอแค่ Normal Condition
contingency_table = pd.crosstab(df['Neighborhood'], df['Condition1'])
plt.figure(figsize=(14, 10))
sns.heatmap(contingency_table, annot=True, cmap='YlGnBu', fmt='g')
plt.title('Relationship between Condition1 and Neighborhood')
plt.xlabel('Condition1')
plt.ylabel('Neighborhood')
plt.tight_layout()
plt.show()
df['YearsBeforeSale'] = df['YrSold'] - df['YearBuilt']
average_prices = df.groupby('YearsBeforeSale')['SalePrice'].mean()
plt.figure(figsize=(10, 6))
sns.histplot(df['YearsBeforeSale'], kde=True, color='lightblue')
ax2 = plt.gca().twinx()
plt.title('Distribution of Years from Build to Sale & Average Sale Price')
plt.xlabel('Years Before Sale')
plt.ylabel('Count')
plt.tight_layout()
plt.show()
อีกคำถามที่น่าสนใจในมุมของคนซื้อบ้านคืออายุบ้านประมาณไหนที่ควรจะทำการ Renovate ? วิธีการวิเคราะห์ข้อมูลตรงนี้ขอเลือกใช้การพล็อต Histogram เพื่อดูความถี่ของจำนวนปีที่มีการ Renovate
# ไม่เอาปีที่ Remodel เท่ากับปีที่สร้าง
remodeled_df = df[df['YearRemodAdd'] != df['YearBuilt']]
remodeled_df['YearsBeforeRemodel'] = remodeled_df['YearRemodAdd'] - remodeled_df['YearBuilt']
plt.figure(figsize=(10, 6))
sns.histplot(remodeled_df['YearsBeforeRemodel'], kde=True, bins=30)
plt.title('Distribution of Years from Build to Remodel')
plt.xlabel('Years Before Remodel')
plt.ylabel('Count')
plt.tight_layout()
plt.show()
จากภาพจะเห็นความแปลกอย่างหนึ่งคือในช่วงบ้านจะถูกปรับ Model ในช่วง 1 ปีหลังการสร้าง จากน้นจะเริ่มมีความถี่เยอะขึ้นจนมาอยู่ในช่วง 30 ปี ถึงจะทำการ Remodel อีกที
คำถามที่ Data Analyst ควรสืบค้นต่อไปจากข้อมูลชุดนี้คือ มันเกิดอะไรขึ้นในปีที่ 1 ทำไมคนถึงเลือกที่จะ Renovate ช่วงนี้บ่อยกันแน่ ? แต่ด้วยความที่ข้อมูลไม่เพียงพอจึงจะขอข้ามประเด็นนี้ไปก่อน
ในช่วงปี 2008-2009 สหรัฐได้เกิดวิกฤต SubPrime ขึ้น เราลองมาดูกันดีกว่าว่าในช่วงวิกฤตนั้น เราจะได้บ้านราคาถูกหรือไม่
grouped_sales = df.groupby(['YrSold', 'PriceCategory']).size().reset_index(name='Number of Houses Sold')
plt.figure(figsize=(12, 7))
sns.lineplot(data=grouped_sales, x='YrSold', y='Number of Houses Sold', hue='PriceCategory', marker='o')
plt.title('Trend of Houses Sold Over the Years by Price Category')
plt.xlabel('Year Sold')
plt.ylabel('Number of Houses Sold')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.tight_layout()
plt.legend(title='Price Category')
plt.show()
คำถามของคำตอบแรกคือหลังปี 2009 แนวโน้มการสร้างบนลดลงอย่างเห็นได้ชัด โดยเฉพาะกลุ่มบ้านราคา Medium Price แต่จุดที่หน้าแปลกใจคือจำนวนการสร้างของอีกสามกลุ่มกลับเร่งตัวขึ้น
grouped_sales_avg = df.groupby(['YrSold', 'PriceCategory'])['SalePrice'].mean().reset_index()
grouped_sales_avg['Percentage Change'] = grouped_sales_avg.groupby('PriceCategory')['SalePrice'].pct_change() * 100
plt.figure(figsize=(12, 7))
sns.lineplot(data=grouped_sales_avg, x='YrSold', y='Percentage Change', hue='PriceCategory', marker='o')
plt.title('Percentage Change in Average Sale Price Over the Years by Price Category')
plt.xlabel('Year Sold')
plt.ylabel('Percentage Change in Average Sale Price')
plt.axhline(0, color='grey', linestyle='--')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.tight_layout()
plt.legend(title='Price Category')
plt.show()
ในส่วนของราคาบ้าน จากภาพด้านบนเราพอจะเห็นว่าในช่วงวิกฤต(ปี 2008) ราคาบ้านกลุ่ม Luxury ปรับตัวลงเกือบ 15% ตามมาด้วยบ้านราคาถูก ที่ปรับตัวลงไป -5% กว่าๆ ทั้งนี้สามารถสรุปได้ว่าถ้าเราอยากได้สินทรัพย์ชิ้นใหญ่ๆ ในราคาถูก ควรรอวิกฤต ในทางตรงกันข้าม บ้านสำหรับอยู่อาศัยทั่วไป ถึงแม้จะเกิดวิกฤต ราคาก็ไม่ได้ปรับตัวลงมากนัก
ตอนนี้ก่อนทำตั้งธงในใจไปก่อนว่าบ้านราคาถูกๆไปถึงกลางๆ ควรที่จะอยู่ในบริเวณที่ประชากรเยอะๆ (Residental High Density)
แต่พอได้ลองเล่นกับข้อมูลดูพบว่าบ้านราคากลางๆ ค่อนไปทางสูง อยู่ในโซน Residental Low Density)
แต่อันที่ไม่ต้องสืบเลยคือบ้านกลางน้ำ (Floating Village Residential) แพงสุด
จากการหาข้อมูลเพิ่มเติมพบว่าส่วนหนึ่งที่ทำให้ยังเป็น Medium Density ได้อยู่เพราะความหนาแน่นประชากรของสหรัฐต่ำ
โดยอ้างอิงจากข้อมูลของปี 2022 พบว่าความหนาแน่นของประชากรคือ
grouped_mean = df.groupby('MSZoning')['SalePrice'].mean().sort_values(ascending=False).reset_index()
plt.figure(figsize=(10, 6))
sns.barplot(x='MSZoning', y='SalePrice', data=grouped_mean, palette='viridis')
plt.title('Average SalePrice by MSZoning')
plt.xlabel('MSZoning')
plt.ylabel('Average SalePrice')
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()
average_prices = df.groupby('Neighborhood')['SalePrice'].mean().sort_values(ascending=False)
sorted_neighborhoods = average_prices.index.tolist()
df['Neighborhood'] = pd.Categorical(df['Neighborhood'], categories=sorted_neighborhoods, ordered=True)
cross_matrix = pd.crosstab(df['Neighborhood'], df['MSZoning'])
plt.figure(figsize=(12, 10))
sns.heatmap(cross_matrix, annot=True, cmap='viridis', cbar=True, fmt="d")
plt.title('MSZoning by Neighborhood')
plt.xlabel('MSZoning')
plt.ylabel('Neighborhood')
plt.tight_layout()
plt.show()
อันนี้เคยเข้าใจว่าวางเงินดาวน์เยอะๆ แล้วบ้านจะถูกลง ปรากฏว่าไม่ใช่ บ้านขายถูกทางวางดาวน์น้อยๆ แต่ติดสัญญาไว้
ทีนี้ถ้าเห็นข้อมูลแบบนี้มันอาจจะสรุปโดยตรงไม่ได้ ขอเดาต่อว่ามันอาจจะเกิดจากคนมีเงินน้อย แล้วซื้อบ้านราคาถูกก็ได้ เลย Plot Heatmap ซ้ำอีกรอบเทียบเพื่อความชัวร์
plt.figure(figsize=(12, 7))
sns.barplot(data=df, x='SaleType', y='SalePrice', estimator=np.mean, ci=None, order=df.groupby('SaleType')['SalePrice'].mean().sort_values(ascending=False).index)
plt.title('Average SalePrice by SaleType')
plt.xlabel('SaleType')
plt.ylabel('Average SalePrice')
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()
ข้อนี้เกิดจากความสงสัยว่าเมื่อระยะเวลาผ่านไป พฤติกรรมของคนขายบ้านเปลี่ยนไปมากน้อยขนาดไหน
ทำไมเรื่องนี้ถึงสำคัญ
เพราะบางทีบริษัทอสังหาริมทรัพย์ก็จัดโปรขายบ้านเพื่อเอาเงินสดมาก่อน โดยไม่ได้ดูคุณภาพของลูกค้า และแน่นอนว่ามันอาจจะส่งผลไปยังวันที่ลูกหนี้ไม่มีปัญญาจ่าย และเป็นสาเหตุของ NPA ในอนาคตได้
โดยจากข้อมูล เราเอาวิธีการขายมาทำเป็น Percent แล้วดูแนวโน้มว่าเป็นอย่างไรผ่าน Line-Chart
โดยข้อสรุปคือแนวโน้มการขายบ้านที่สร้างสำเร็จในช่วง 20 ปีให้หลังเน้นไปที่การตีราคาและขายก่อนที่บ้านจะสร้างเสร็จด้วยซ้ำ
ในขณะที่บ้านที่สร้างก่อนหน้าจะนิยมขายแบบปกติ
เลยอาจจะเดาได้ว่าลักษณะของผู้อยู่อาศัยเปลี่ยน พฤติกรรมผู้บริโภคอาจเปลี่ยนตาม
grouped = df.groupby(['DecadeBuilt', 'SaleCondition']).size().reset_index(name='Count')
pivot_df = grouped.pivot(index='DecadeBuilt', columns='SaleCondition', values='Count').fillna(0)
#ทำเป็น Percent
percentage_df = pivot_df.divide(pivot_df.sum(axis=1), axis=0) * 100
grouped = df.groupby(['DecadeBuilt', 'SaleCondition']).size().reset_index(name='Count')
ax = percentage_df.plot(kind='line', marker='o', figsize=(10, 7))
plt.title('Percentage of SaleCondition by DecadeBuilt')
plt.ylabel('Percentage')
plt.xlabel('Decade Built')
plt.legend(title='SaleCondition')
plt.show()
ทีนี้ลองเอาค่าเฉลี่ยของการขายบ้านแต่ละกลุ่มมาเทียบกันดีกว่า Sale Condition แบบไหนจะทำให้ได้ราคาบ้านดีที่สุด
ซึ่งหลักฐานมันก็ชี้ชัดแล้วว่าการขายในภาวะไม่ปกติ ทั้งการขายแบบ Short Sale การแลกบ้าน ทำให้ได้ราคาดี
แต่ที่น่าแปลกคือการขายที่ดินที่ติดกัน กลับได้ราคาถูกทั้งถ้าเป็นย่านดีๆ มันควรราคาแพง อาจจะต้องลง Detail ต่อไป
แต่ส่วนตัวมองว่ามันอาจจะยังสรุปไม่ได้เพราะชุดข้อมูลน้อยเกินไป
# First, let's aggregate the data by SaleType
sale_type_data = df.groupby('SaleCondition')['SalePrice'].agg(['count', 'mean'])
# Sort the data by sales count
sale_type_data.sort_values('count', ascending=False, inplace=True)
# Reset the index for ease of plotting
sale_type_data.reset_index(inplace=True)
# Create a figure and axis
fig, ax1 = plt.subplots(figsize=(10,6))
# Bar plot for the count of sales per SaleType
sns.barplot(x='SaleCondition', y='count', data=sale_type_data, ax=ax1, palette="Blues_d")
ax1.set_ylabel('Number of Sales')
ax1.set_xlabel('Sale Type')
# Create a second y-axis for the average sale price
ax2 = ax1.twinx()
# Line plot for the average sale price per SaleType
sns.lineplot(x='SaleCondition', y='mean', data=sale_type_data, ax=ax2, color='r', sort=False)
ax2.set_ylabel('Average Sale Price')
# Rotate x-axis labels for better readability
ax1.set_xticklabels(ax1.get_xticklabels(), rotation=90)
# Set the title for the plot
plt.title('Number of Sales and Average Sale Price by SaleCondition')
plt.tight_layout()
plt.show()
อันนี้พอดีเกิดความสงสัยส่วนตัวว่าตลอดเวลาที่ผ่านมา บ้านที่มีสระว่ายน้ำเทียบกับไม่มีเยอะหรือไม่ เลยลองทำดู พบว่าจริงๆแล้วมีบ้านแค่ 0.5% เท่านั้นที่มีสระว่ายน้ำ
import matplotlib.pyplot as plt
pool = (df['PoolArea'] > 0).sum()
no_pool = (df['PoolArea'] == 0).sum()
labels = 'With Pool', 'Without Pool'
sizes = [pool, no_pool]
colors = ['gold', 'lightskyblue']
explode = (0.1, 0)
plt.figure(figsize=(10, 6))
plt.pie(sizes, explode=explode, labels=labels, colors=colors,
autopct='%1.1f%%', shadow=True, startangle=140)
plt.axis('equal')
plt.title('Distribution of Houses With and Without a Pool')
plt.show()
จากที่ลองเล่นกับข้อมูลมา Insight ที่พอจะหาได้คือ
คำถามที่ควรถามต่อ
สิ่งที่พอทำต่อได้กับ Data ชุดนี้
ข้อเสนอแนะ